diff --git a/Cargo.toml b/Cargo.toml index 0b8836ad..db5f7be9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,10 +32,11 @@ config_rs = { package = "config", version = "0.11.0", features = ["toml"] } fluent-templates = "0.6.1" unic-langid = "0.9.0" -actix-web = "4.0.0-rc.3" -sycamore = { version = "0.8.0-beta.2", features = ["ssr"] } +actix-web = "3.3.3" +maud = { version = "0.23.0", features = ["actix-web"] } +sycamore = { version = "0.7.1", features = ["ssr"] } +downcast-rs = "1.2.0" -tokio = { version = "1.16", features = ["macros", "rt-multi-thread"] } serde = { version = "1.0", features = ["derive"] } [lib] diff --git a/STARTER.Cargo.toml b/STARTER.Cargo.toml index 13ef591d..1db51f3d 100644 --- a/STARTER.Cargo.toml +++ b/STARTER.Cargo.toml @@ -8,7 +8,5 @@ edition = "2021" [dependencies] pagetop = { path = "pagetop" } -tokio = { version = "1.16", features = ["macros", "rt-multi-thread"] } - -[[bin]] -name = "app" +actix-web = "3.3.3" +maud = { version = "0.23.0" } diff --git a/config/default.toml b/config/default.toml index 429d6da5..f96d1a7d 100644 --- a/config/default.toml +++ b/config/default.toml @@ -1,2 +1,3 @@ [app] name = "PageTop Essence" +language = "es-ES" diff --git a/config/settings.default.toml b/config/settings.default.toml index dbbf8355..c726c44e 100644 --- a/config/settings.default.toml +++ b/config/settings.default.toml @@ -3,6 +3,8 @@ name = "PageTop Application" description = "Developed with the amazing PageTop framework." # Idioma (localización) predeterminado. language = "en-US" +# Tema predeterminado. +theme = "Minimal" [webserver] # Configuración opcional del servidor web. diff --git a/src/base/component/chunck.rs b/src/base/component/chunck.rs new file mode 100644 index 00000000..29c62918 --- /dev/null +++ b/src/base/component/chunck.rs @@ -0,0 +1,77 @@ +use crate::prelude::*; + +pub struct Chunck { + renderable: fn() -> bool, + weight : i8, + markup : Vec, + template : String, +} + +impl PageComponent for Chunck { + + fn prepare() -> Self { + Chunck { + renderable: always, + weight : 0, + markup : Vec::new(), + template : "default".to_string(), + } + } + + fn is_renderable(&self) -> bool { + (self.renderable)() + } + + fn weight(&self) -> i8 { + self.weight + } + + fn default_render(&self, _: &mut PageAssets) -> Markup { + html! { + @for markup in self.markup.iter() { + (*markup) + } + } + } +} + +impl Chunck { + + // Chunck BUILDER. + + pub fn markup(markup: Markup) -> Self { + let mut chunck = Chunck::prepare(); + chunck.markup.push(markup); + chunck + } + + pub fn with_renderable(mut self, renderable: fn() -> bool) -> Self { + self.renderable = renderable; + self + } + + pub fn with_weight(mut self, weight: i8) -> Self { + self.weight = weight; + self + } + + pub fn add_markup(mut self, markup: Markup) -> Self { + self.markup.push(markup); + self + } + + pub fn using_template(mut self, template: &str) -> Self { + self.template = template.to_string(); + self + } + + // Chunck GETTERS. + + pub fn template(&self) -> &str { + self.template.as_str() + } +} + +fn always() -> bool { + true +} diff --git a/src/base/component/container.rs b/src/base/component/container.rs new file mode 100644 index 00000000..58a0caac --- /dev/null +++ b/src/base/component/container.rs @@ -0,0 +1,103 @@ +use crate::prelude::*; + +enum ContainerType { Column, Row, Wrapper } + +pub struct Container { + renderable: fn() -> bool, + weight : i8, + id : String, + container : ContainerType, + components: PageContainer, + template : String, +} + +impl PageComponent for Container { + + fn prepare() -> Self { + Container { + renderable: always, + weight : 0, + id : "".to_string(), + container : ContainerType::Wrapper, + components: PageContainer::new(), + template : "default".to_string(), + } + } + + fn is_renderable(&self) -> bool { + (self.renderable)() + } + + fn weight(&self) -> i8 { + self.weight + } + + fn default_render(&self, assets: &mut PageAssets) -> Markup { + let classes = match self.container { + ContainerType::Wrapper => "wrapper", + ContainerType::Row => "row", + ContainerType::Column => "col", + }; + html! { + div class=(classes) { + (self.components.render(assets)) + } + } + } +} + +impl Container { + + // Container BUILDER. + + pub fn row() -> Self { + let mut grid = Container::prepare(); + grid.container = ContainerType::Row; + grid + } + + pub fn column() -> Self { + let mut grid = Container::prepare(); + grid.container = ContainerType::Column; + grid + } + + pub fn with_renderable(mut self, renderable: fn() -> bool) -> Self { + self.renderable = renderable; + self + } + + pub fn with_weight(mut self, weight: i8) -> Self { + self.weight = weight; + self + } + + pub fn with_id(mut self, id: &str) -> Self { + self.id = id.to_string(); + self + } + + pub fn add(mut self, component: impl PageComponent) -> Self { + self.components.add(component); + self + } + + pub fn using_template(mut self, template: &str) -> Self { + self.template = template.to_string(); + self + } + + // Grid GETTERS. + + pub fn id(&self) -> &str { + self.id.as_str() + } + + pub fn template(&self) -> &str { + self.template.as_str() + } +} + +fn always() -> bool { + true +} diff --git a/src/base/component/mod.rs b/src/base/component/mod.rs new file mode 100644 index 00000000..c12fb175 --- /dev/null +++ b/src/base/component/mod.rs @@ -0,0 +1,2 @@ +pub mod container; +pub mod chunck; diff --git a/src/base/mod.rs b/src/base/mod.rs index fadf78f5..f40d2d0e 100644 --- a/src/base/mod.rs +++ b/src/base/mod.rs @@ -1,3 +1,5 @@ //! Temas, Módulos y Componentes base. +pub mod theme; pub mod module; +pub mod component; diff --git a/src/base/module/homepage/locales/en-US/homepage.ftl b/src/base/module/homepage/locales/en-US/homepage.ftl index 380b29ee..7008c35a 100644 --- a/src/base/module/homepage/locales/en-US/homepage.ftl +++ b/src/base/module/homepage/locales/en-US/homepage.ftl @@ -1,4 +1,4 @@ module_name = Default homepage module_desc = Displays a default homepage when none is configured. -greeting = Hello { $name }! +greetings = Hello { $name }! diff --git a/src/base/module/homepage/locales/es-ES/homepage.ftl b/src/base/module/homepage/locales/es-ES/homepage.ftl index 46571c42..7af7b370 100644 --- a/src/base/module/homepage/locales/es-ES/homepage.ftl +++ b/src/base/module/homepage/locales/es-ES/homepage.ftl @@ -1,4 +1,4 @@ module_name = Página de inicio predeterminada module_desc = Muestra una página de inicio predeterminada cuando no hay ninguna configurada. -greeting = Hola { $name }! +greetings = ¡Hola { $name }! diff --git a/src/base/module/homepage/mod.rs b/src/base/module/homepage/mod.rs index 637659cd..4133f6ba 100644 --- a/src/base/module/homepage/mod.rs +++ b/src/base/module/homepage/mod.rs @@ -14,26 +14,16 @@ impl Module for HomepageModule { } fn configure_module(&self, cfg: &mut server::web::ServiceConfig) { - cfg.service( - server::web::resource("/") - .route(server::web::get().to(greet)) - ); - cfg.service( - server::web::resource("/{name}") - .route(server::web::get().to(greet_with_param)) - ); + cfg.service(server::web::resource("/").to(home)); + cfg.service(server::web::resource("/{name}").to(home)); } } -async fn greet() -> impl server::Responder { - t("greeting", &args!["name" => config_get!("app.name")]) -} - -async fn greet_with_param(req: server::HttpRequest) -> server::HttpResponse { +async fn home(req: server::HttpRequest) -> server::Result { let name: String = req.match_info().get("name").unwrap_or("World").into(); - let args = args!["name" => name]; - server::HttpResponse::Ok() - .body(sycamore::render_to_string(|ctx| sycamore::view! { ctx, - p { (t("greeting", &args)) } + Page::prepare() + .add_to("content", Chunck::markup(html! { + h1 { (t("greetings", &args![ "name" => name])) } })) + .render() } diff --git a/src/base/theme/minimal/mod.rs b/src/base/theme/minimal/mod.rs new file mode 100644 index 00000000..ea1ea1f4 --- /dev/null +++ b/src/base/theme/minimal/mod.rs @@ -0,0 +1,9 @@ +use crate::prelude::*; + +pub struct MinimalTheme; + +impl Theme for MinimalTheme { + fn name(&self) -> &str { + "Minimal" + } +} diff --git a/src/base/theme/mod.rs b/src/base/theme/mod.rs new file mode 100644 index 00000000..b272be88 --- /dev/null +++ b/src/base/theme/mod.rs @@ -0,0 +1 @@ +pub mod minimal; diff --git a/src/config/settings.rs b/src/config/settings.rs index 88c5805d..3c9a7853 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -73,6 +73,7 @@ pub struct App { pub name : String, pub description : String, pub language : String, + pub theme : String, pub run_mode : String, } @@ -98,6 +99,7 @@ Ajustes globales y valores predeterminados para las secciones *\[app\]* y "app.name" => "PageTop Application", "app.description" => "Developed with the amazing PageTop framework.", "app.language" => "en-US", + "app.theme" => "Minimal", // [webserver] "webserver.bind_address" => "localhost", diff --git a/src/core/all.rs b/src/core/all.rs index 4067a838..716dac9b 100644 --- a/src/core/all.rs +++ b/src/core/all.rs @@ -1,5 +1,11 @@ use crate::core::{server, state}; +pub fn themes(cfg: &mut server::web::ServiceConfig) { + for t in state::THEMES.read().unwrap().iter() { + t.configure_theme(cfg); + } +} + pub fn modules(cfg: &mut server::web::ServiceConfig) { for m in state::MODULES.read().unwrap().iter() { m.configure_module(cfg); diff --git a/src/core/mod.rs b/src/core/mod.rs index 3c6040c9..de375c10 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,9 +1,13 @@ pub use actix_web::dev::Server; mod state; +pub use state::register_theme; pub use state::register_module; +pub use state::add_component_to; mod all; +pub mod theme; pub mod module; +pub mod response; pub mod server; diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs new file mode 100644 index 00000000..79d28612 --- /dev/null +++ b/src/core/response/mod.rs @@ -0,0 +1 @@ +pub mod page; diff --git a/src/core/response/page/assets.rs b/src/core/response/page/assets.rs new file mode 100644 index 00000000..d36c870e --- /dev/null +++ b/src/core/response/page/assets.rs @@ -0,0 +1,258 @@ +use crate::core::theme::{Markup, PreEscaped, html}; + +// ----------------------------------------------------------------------------- +// Favicon. +// ----------------------------------------------------------------------------- + +pub struct Favicon(Vec); + +impl Favicon { + pub fn new() -> Self { + Favicon(Vec::new()) + } + + pub fn with_icon(self, image: &str) -> Self { + self.add_item("icon", image, "", "") + } + + pub fn with_icon_for_sizes(self, image: &str, sizes: &str) -> Self { + self.add_item("icon", image, sizes, "") + } + + pub fn with_apple_touch_icon(self, image: &str, sizes: &str) -> Self { + self.add_item("apple-touch-icon", image, sizes, "") + } + + pub fn with_mask_icon(self, image: &str, color: &str) -> Self { + self.add_item("mask-icon", image, "", color) + } + + pub fn with_manifest(self, file: &str) -> Self { + self.add_item("manifest", file, "", "") + } + + pub fn with_theme_color(mut self, color: &str) -> Self { + self.0.push(format!( + "", color + )); + self + } + + pub fn with_ms_tile_color(mut self, color: &str) -> Self { + self.0.push(format!( + "", color + )); + self + } + + pub fn with_ms_tile_image(mut self, image: &str) -> Self { + self.0.push(format!( + "", image + )); + self + } + + fn add_item( + mut self, + rel : &str, + source: &str, + sizes : &str, + color : &str + ) -> Self { + let mut link: String = format!(" format!("{} type=\"image/gif\"", link), + ".ico" => format!("{} type=\"image/x-icon\"", link), + ".jpg" => format!("{} type=\"image/jpg\"", link), + ".png" => format!("{} type=\"image/png\"", link), + ".svg" => format!("{} type=\"image/svg+xml\"", link), + _ => link + }; + } + if !sizes.is_empty() { + link = format!("{} sizes=\"{}\"", link, sizes); + } + if !color.is_empty() { + link = format!("{} color=\"{}\"", link, color); + } + self.0.push(format!("{} href=\"{}\">", link, source)); + self + } + + fn render(&self) -> Markup { + html! { + @for item in &self.0 { + (PreEscaped(item)) + } + } + } +} + +// ----------------------------------------------------------------------------- +// StyleSheet. +// ----------------------------------------------------------------------------- + +pub struct StyleSheet { + source: &'static str, + weight: i8, +} +impl StyleSheet { + pub fn source(s: &'static str) -> Self { + StyleSheet { + source: s, + weight: 0, + } + } + + pub fn with_weight(mut self, weight: i8) -> Self { + self.weight = weight; + self + } + + pub fn weight(self) -> i8 { + self.weight + } + + fn render(&self) -> Markup { + html! { + link rel="stylesheet" href=(self.source); + } + } +} + +// ----------------------------------------------------------------------------- +// JavaScript. +// ----------------------------------------------------------------------------- + +#[derive(PartialEq)] +pub enum JSMode { Async, Defer, Normal } + +pub struct JavaScript { + source: &'static str, + weight: i8, + mode : JSMode, +} +impl JavaScript { + pub fn source(s: &'static str) -> Self { + JavaScript { + source: s, + weight: 0, + mode : JSMode::Defer, + } + } + + pub fn with_weight(mut self, weight: i8) -> Self { + self.weight = weight; + self + } + + pub fn with_mode(mut self, mode: JSMode) -> Self { + self.mode = mode; + self + } + + pub fn weight(self) -> i8 { + self.weight + } + + fn render(&self) -> Markup { + html! { + script type="text/javascript" + src=(self.source) + async[self.mode == JSMode::Async] + defer[self.mode == JSMode::Defer] + {}; + } + } +} + +// ----------------------------------------------------------------------------- +// Page assets. +// ----------------------------------------------------------------------------- + +pub struct Assets { + favicon : Option, + metadata : Vec<(String, String)>, + stylesheets: Vec, + javascripts: Vec, + seqid_count: u16, +} + +impl Assets { + pub fn new() -> Self { + Assets { + favicon : None, + metadata : Vec::new(), + stylesheets: Vec::new(), + javascripts: Vec::new(), + seqid_count: 0, + } + } + + pub fn with_favicon(&mut self, favicon: Favicon) -> &mut Self { + self.favicon = Some(favicon); + self + } + + pub fn add_metadata(&mut self, name: String, content: String) -> &mut Self { + self.metadata.push((name, content)); + self + } + + pub fn add_stylesheet(&mut self, css: StyleSheet) -> &mut Self { + match self.stylesheets.iter().position(|x| x.source == css.source) { + Some(index) => if self.stylesheets[index].weight > css.weight { + self.stylesheets.remove(index); + self.stylesheets.push(css); + }, + _ => self.stylesheets.push(css) + } + self + } + + pub fn add_javascript(&mut self, js: JavaScript) -> &mut Self { + match self.javascripts.iter().position(|x| x.source == js.source) { + Some(index) => if self.javascripts[index].weight > js.weight { + self.javascripts.remove(index); + self.javascripts.push(js); + }, + _ => self.javascripts.push(js) + } + self + } + + pub fn render(&mut self) -> Markup { + let ordered_css = &mut self.stylesheets; + ordered_css.sort_by_key(|o| o.weight); + + let ordered_js = &mut self.javascripts; + ordered_js.sort_by_key(|o| o.weight); + + html! { + @match &self.favicon { + Some(favicon) => (favicon.render()), + None => "", + } + @for (name, content) in &self.metadata { + meta name=(name) content=(content) {} + } + @for css in ordered_css { + (css.render()) + } + @for js in ordered_js { + (js.render()) + } + } + } + + // Assets GETTERS. + + pub fn seqid(&mut self, id: &str) -> String { + if id.is_empty() { + self.seqid_count += 1; + return format!("seqid-{}", self.seqid_count); + } + id.to_string() + } +} diff --git a/src/core/response/page/component.rs b/src/core/response/page/component.rs new file mode 100644 index 00000000..097d960f --- /dev/null +++ b/src/core/response/page/component.rs @@ -0,0 +1,34 @@ +use crate::core::theme::{Markup, html}; +use crate::core::response::page::PageAssets; + +use downcast_rs::{Downcast, impl_downcast}; + +use std::any::type_name; + +pub trait Component: Downcast + Send + Sync { + + fn prepare() -> Self where Self: Sized; + + fn qualified_name(&self) -> &str { + type_name::() + } + + fn description(&self) -> &str { + "" + } + + fn is_renderable(&self) -> bool { + true + } + + fn weight(&self) -> i8 { + 0 + } + + #[allow(unused_variables)] + fn default_render(&self, assets: &mut PageAssets) -> Markup { + html! {} + } +} + +impl_downcast!(Component); diff --git a/src/core/response/page/container.rs b/src/core/response/page/container.rs new file mode 100644 index 00000000..c9d2e13b --- /dev/null +++ b/src/core/response/page/container.rs @@ -0,0 +1,33 @@ +use crate::core::theme::{Markup, html}; +use crate::core::response::page::{PageAssets, PageComponent, render_component}; + +use std::sync::Arc; + +#[derive(Clone)] +pub struct Container(Vec>); + +impl Container { + pub fn new() -> Self { + Container(Vec::new()) + } + + pub fn new_with(component: impl PageComponent) -> Self { + let mut container = Container::new(); + container.add(component); + container + } + + pub fn add(&mut self, component: impl PageComponent) { + self.0.push(Arc::new(component)); + } + + pub fn render(&self, assets: &mut PageAssets) -> Markup { + let mut components = self.0.clone(); + components.sort_by_key(|c| c.weight()); + html! { + @for c in components.iter() { + (render_component(&**c, assets)) + } + } + } +} \ No newline at end of file diff --git a/src/core/response/page/mod.rs b/src/core/response/page/mod.rs new file mode 100644 index 00000000..881e0df3 --- /dev/null +++ b/src/core/response/page/mod.rs @@ -0,0 +1,12 @@ +pub mod assets; +pub use assets::Assets as PageAssets; + +mod component; +pub use component::Component as PageComponent; + +mod container; +pub use container::Container as PageContainer; + +mod page; +pub use page::Page; +pub use page::render_component; diff --git a/src/core/response/page/page.rs b/src/core/response/page/page.rs new file mode 100644 index 00000000..add4c64d --- /dev/null +++ b/src/core/response/page/page.rs @@ -0,0 +1,172 @@ +use crate::config::SETTINGS; +use crate::core::server; +use crate::core::state::{COMPONENTS, THEME}; +use crate::core::theme::{DOCTYPE, Markup, html}; +use crate::core::response::page::{PageAssets, PageComponent, PageContainer}; + +use std::borrow::Cow; +use std::collections::HashMap; + +pub enum TextDirection { LeftToRight, RightToLeft, Auto } + +pub struct Page<'a> { + language : &'a str, + title : &'a str, + direction : &'a str, + description : &'a str, + body_classes: Cow<'a, str>, + assets : PageAssets, + regions : HashMap<&'a str, PageContainer>, + template : &'a str, +} + +impl<'a> Page<'a> { + + pub fn prepare() -> Self { + Page { + language : &SETTINGS.app.language[..2], + title : &SETTINGS.app.name, + direction : "ltr", + description : "", + body_classes: "body".into(), + assets : PageAssets::new(), + regions : COMPONENTS.read().unwrap().clone(), + template : "default", + } + } + + // Page BUILDER. + + pub fn with_language(&mut self, language: &'a str) -> &mut Self { + self.language = language; + self + } + + pub fn with_title(&mut self, title: &'a str) -> &mut Self { + self.title = title; + self + } + + pub fn with_direction(&mut self, dir: TextDirection) -> &mut Self { + self.direction = match dir { + TextDirection::LeftToRight => "ltr", + TextDirection::RightToLeft => "rtl", + _ => "auto" + }; + self + } + + pub fn with_description(&mut self, description: &'a str) -> &mut Self { + self.description = description; + self + } + + pub fn with_body_classes(&mut self, body_classes: &'a str) -> &mut Self { + self.body_classes = body_classes.into(); + self + } + + pub fn add_body_classes(&mut self, body_classes: &'a str) -> &mut Self { + self.body_classes = String::from( + format!("{} {}", self.body_classes, body_classes).trim() + ).into(); + self + } + + pub fn add_to( + &mut self, + region: &'a str, + component: impl PageComponent + ) -> &mut Self { + if let Some(regions) = self.regions.get_mut(region) { + regions.add(component); + } else { + self.regions.insert(region, PageContainer::new_with(component)); + } + self + } + + pub fn using_template(&mut self, template: &'a str) -> &mut Self { + self.template = template; + self + } + + // Page GETTERS. + + pub fn language(&self) -> &str { + self.language + } + + pub fn title(&self) -> &str { + self.title + } + + pub fn direction(&self) -> TextDirection { + match self.direction { + "ltr" => TextDirection::LeftToRight, + "rtl" => TextDirection::RightToLeft, + _ => TextDirection::Auto + } + } + + pub fn description(&self) -> &str { + self.description + } + + pub fn body_classes(&self) -> &str { + if self.body_classes.is_empty() { + return "body"; + } + &self.body_classes + } + + pub fn assets(&mut self) -> &mut PageAssets { + &mut self.assets + } + + pub fn template(&self) -> &str { + self.template + } + + // Page RENDER. + + pub fn render(&mut self) -> server::Result { + // Acciones del tema antes de renderizar la página. + THEME.before_render_page(self); + + // Primero, renderizar el cuerpo. + let body = THEME.render_page_body(self); + + // Luego, renderizar la cabecera. + let head = THEME.render_page_head(self); + + // Finalmente, renderizar la página. + return Ok(html! { + (DOCTYPE) + html lang=(self.language) dir=(self.direction) { + (head) + (body) + } + }) + } + + pub fn render_region(&mut self, region: &str) -> Markup { + match self.regions.get_mut(region) { + Some(components) => components.render(&mut self.assets), + None => html! {} + } + } +} + +pub fn render_component( + component: &dyn PageComponent, + assets: &mut PageAssets +) -> Markup { + match component.is_renderable() { + true => match THEME.render_component(component, assets) { + Some(markup) => markup, + None => component.default_render(assets) + }, + false => html! {} + } +} diff --git a/src/core/server/main.rs b/src/core/server/main.rs index 49e78a65..ea0deafc 100644 --- a/src/core/server/main.rs +++ b/src/core/server/main.rs @@ -8,13 +8,14 @@ pub fn run(bootstrap: Option) -> Result { let _ = &(bootstrap.unwrap())(); } - // Registra la página de inicio de PageTop como último módulo. - // Así, la función de arranque de la aplicación podría sobrecargarlo. + // Registra el módulo para la página de inicio de PageTop. + // Al ser el último, puede sobrecargarse en la función de arranque. register_module(&base::module::homepage::HomepageModule); // Inicializa el servidor web. let server = server::HttpServer::new(|| { server::App::new() + .configure(&all::themes) .configure(&all::modules) }) .bind(format!("{}:{}", diff --git a/src/core/server/mod.rs b/src/core/server/mod.rs index d39c1d4d..713a7ae6 100644 --- a/src/core/server/mod.rs +++ b/src/core/server/mod.rs @@ -1,4 +1,6 @@ -pub use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, web}; +pub use actix_web::{ + App, HttpRequest, HttpResponse, HttpServer, Responder, Result, web +}; mod main; pub use main::run; diff --git a/src/core/state.rs b/src/core/state.rs index 5535c3b6..69b6a4de 100644 --- a/src/core/state.rs +++ b/src/core/state.rs @@ -1,7 +1,35 @@ use crate::Lazy; +use crate::config::SETTINGS; +use crate::core::theme::Theme; use crate::core::module::Module; +use crate::core::response::page::{PageComponent, PageContainer}; +use crate::base; use std::sync::RwLock; +use std::collections::HashMap; + +// ----------------------------------------------------------------------------- +// Temas registrados. +// ----------------------------------------------------------------------------- + +pub static THEMES: Lazy>> = Lazy::new(|| { + RwLock::new(vec![ + &base::theme::minimal::MinimalTheme, + ]) +}); + +pub static THEME: Lazy<&dyn Theme> = Lazy::new(|| { + for t in THEMES.read().unwrap().iter() { + if t.name().to_lowercase() == SETTINGS.app.theme.to_lowercase() { + return *t; + } + } + &base::theme::minimal::MinimalTheme +}); + +pub fn register_theme(t: &'static (dyn Theme + 'static)) { + THEMES.write().unwrap().push(t); +} // ----------------------------------------------------------------------------- // Módulos registrados. @@ -14,3 +42,21 @@ pub static MODULES: Lazy>> = Lazy::new(|| { pub fn register_module(m: &'static (dyn Module + 'static)) { MODULES.write().unwrap().push(m); } + +// ----------------------------------------------------------------------------- +// Componentes globales. +// ----------------------------------------------------------------------------- + +pub static COMPONENTS: Lazy>> = Lazy::new( + || { RwLock::new(HashMap::new()) } +); + +#[allow(dead_code)] +pub fn add_component_to(region: &'static str, component: impl PageComponent) { + let mut hmap = COMPONENTS.write().unwrap(); + if let Some(regions) = hmap.get_mut(region) { + regions.add(component); + } else { + hmap.insert(region, PageContainer::new_with(component)); + } +} diff --git a/src/core/theme/api.rs b/src/core/theme/api.rs new file mode 100644 index 00000000..26d71552 --- /dev/null +++ b/src/core/theme/api.rs @@ -0,0 +1,84 @@ +use crate::core::server; +use crate::core::theme::{Markup, html}; +use crate::core::response::page::{Page, PageAssets, PageComponent}; + +/// Los temas deben implementar este "trait". +pub trait Theme: Send + Sync { + fn name(&self) -> &str; + + fn description(&self) -> &str { + "" + } + + #[allow(unused_variables)] + fn configure_theme(&self, cfg: &mut server::web::ServiceConfig) { + } + + #[allow(unused_variables)] + fn before_render_page(&self, page: &mut Page) { + } + + fn render_page_head(&self, page: &mut Page) -> Markup { + let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; + let description = page.description(); + html! { + head { + meta charset="utf-8"; + + meta http-equiv="X-UA-Compatible" content="IE=edge"; + meta name="viewport" content=(viewport); + @if !description.is_empty() { + meta name="description" content=(description); + } + + title { (page.title()) } + + (page.assets().render()) + } + } + } + + fn render_page_body(&self, page: &mut Page) -> Markup { + html! { + body id="body" class=(page.body_classes()) { + @match page.template() { + "admin" => { + @for region in &["top-menu", "side-menu", "content"] { + #(region) { + (page.render_region(region)) + } + } + }, + _ => { + #content { + (page.render_region("content")) + } + } + } + } + } + } + + #[allow(unused_variables)] + fn render_component( + &self, + component: &dyn PageComponent, + assets: &mut PageAssets + ) -> Option { + None + /* + Cómo usarlo: + + match component.type_name() { + "Block" => { + let block = component.downcast_mut::().unwrap(); + match block.template() { + "default" => Some(block_default(block)), + _ => None + } + }, + _ => None + } + */ + } +} diff --git a/src/core/theme/mod.rs b/src/core/theme/mod.rs new file mode 100644 index 00000000..9b908257 --- /dev/null +++ b/src/core/theme/mod.rs @@ -0,0 +1,4 @@ +pub use maud::{DOCTYPE, Markup, PreEscaped, html}; + +mod api; +pub use api::Theme; diff --git a/src/locale/mod.rs b/src/locale/mod.rs index 21fd7bd1..6e64cc8f 100644 --- a/src/locale/mod.rs +++ b/src/locale/mod.rs @@ -43,5 +43,15 @@ macro_rules! localize { ) -> String { LOCALES.lookup_with_args(&LANGID, key, args) } + + #[allow(dead_code)] + pub fn e( + key: &str, + args: &std::collections::HashMap + ) -> crate::core::theme::PreEscaped { + crate::core::theme::PreEscaped( + LOCALES.lookup_with_args(&LANGID, key, args) + ) + } }; } diff --git a/src/main.rs b/src/main.rs index 96cee3df..8559a909 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#[tokio::main] +#[actix_web::main] async fn main() -> std::io::Result<()> { pagetop::core::server::run(None)?.await } diff --git a/src/prelude.rs b/src/prelude.rs index 43370939..c74bb379 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,10 +1,19 @@ //! Re-exporta recursos comunes. pub use crate::args; -pub use crate::config_get; pub use crate::localize; +pub use crate::core::theme::*; + pub use crate::core::module::*; +pub use crate::core::response::page::*; +pub use crate::core::response::page::assets::*; + pub use crate::core::server; +pub use crate::core::register_theme; pub use crate::core::register_module; +pub use crate::core::add_component_to; + +pub use crate::base::component::container::Container; +pub use crate::base::component::chunck::Chunck;