use std::{collections::HashMap, fmt::Display}; use serde::{Deserialize, Serialize}; use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; use crate::model::auth::DefaultTimelineChoice; /// Each different page which can be customized. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum CustomizablePage { Home, All, Popular, } /// Layouts allow you to customize almost every page in the Tetratto UI through /// simple blocks. #[derive(Serialize, Deserialize)] pub struct Layout { pub id: usize, pub created: usize, pub owner: usize, pub title: String, pub privacy: LayoutPrivacy, pub pages: Vec, pub replaces: CustomizablePage, } impl Layout { /// Create a new [`Layout`]. pub fn new(title: String, owner: usize, replaces: CustomizablePage) -> Self { Self { id: Snowflake::new().to_string().parse::().unwrap(), created: unix_epoch_timestamp(), owner, title, privacy: LayoutPrivacy::Public, pages: Vec::new(), replaces, } } } /// The privacy of the layout, which controls who has the ability to view it. #[derive(Serialize, Deserialize, PartialEq, Eq)] pub enum LayoutPrivacy { Public, Private, } impl Display for Layout { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut out = String::new(); for (i, page) in self.pages.iter().enumerate() { let mut x = page.to_string(); if i == 0 { x = x.replace("%?%", ""); } else { x = x.replace("%?%", "hidden"); } out.push_str(&x); } f.write_str(&out) } } /// Layouts are able to contain subpages within them. /// /// Each layout is only allowed 2 subpages pages, meaning one main page and one extra. #[derive(Serialize, Deserialize)] pub struct LayoutPage { pub name: String, pub blocks: Vec, pub css: String, pub js: String, } impl Display for LayoutPage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&format!( "
{}
", { let mut out = String::new(); for block in &self.blocks { out.push_str(&block.to_string()); } out }, self.css, self.js )) } } /// Blocks are the basis of each layout page. They are simple and composable. #[derive(Serialize, Deserialize)] pub struct LayoutBlock { pub r#type: BlockType, pub children: Vec, } impl LayoutBlock { pub fn render_children(&self) -> String { let mut out = String::new(); for child in &self.children { out.push_str(&child.to_string()); } out } } impl Display for LayoutBlock { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut out = String::new(); // head out.push_str(&match self.r#type { BlockType::Block(ref x) => format!("<{} {}>", x.element, x), BlockType::Flexible(ref x) => format!("<{} {}>", x.element, x), BlockType::Markdown(ref x) => format!("<{} {}>", x.element, x), BlockType::Timeline(ref x) => format!("<{} {}>", x.element, x), }); // body out.push_str(&match self.r#type { BlockType::Block(_) => self.render_children(), BlockType::Flexible(_) => self.render_children(), BlockType::Markdown(ref x) => x.sub_options.content.to_string(), BlockType::Timeline(ref x) => { format!( "
", x.sub_options.timeline ) } }); // tail out.push_str(&self.r#type.unwrap_cloned().element.tail()); // ... f.write_str(&out) } } /// Each different type of block has different attributes associated with it. #[derive(Serialize, Deserialize)] pub enum BlockType { Block(GeneralBlockOptions), Flexible(GeneralBlockOptions), Markdown(GeneralBlockOptions), Timeline(GeneralBlockOptions), } impl BlockType { pub fn unwrap(self) -> GeneralBlockOptions> { match self { Self::Block(x) => x.boxed(), Self::Flexible(x) => x.boxed(), Self::Markdown(x) => x.boxed(), Self::Timeline(x) => x.boxed(), } } pub fn unwrap_cloned(&self) -> GeneralBlockOptions> { match self { Self::Block(x) => x.boxed_cloned::(), Self::Flexible(x) => x.boxed_cloned::(), Self::Markdown(x) => x.boxed_cloned::(), Self::Timeline(x) => x.boxed_cloned::(), } } } #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum HtmlElement { Div, Span, Italics, Bold, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Image, } impl HtmlElement { pub fn tail(&self) -> String { match self { Self::Image => String::new(), _ => format!(""), } } } impl Display for HtmlElement { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::Div => "div", Self::Span => "span", Self::Italics => "i", Self::Bold => "b", Self::Heading1 => "h1", Self::Heading2 => "h2", Self::Heading3 => "h3", Self::Heading4 => "h4", Self::Heading5 => "h5", Self::Heading6 => "h6", Self::Image => "img", }) } } /// This trait is used to provide cloning capabilities to structs which DO implement /// clone, but we aren't allowed to tell the compiler that they implement clone /// (through a trait bound), as Clone is not dyn compatible. /// /// Implementations for this trait should really just take reference to another /// value (T), then just run `.to_owned()` on it. This means T and F (Self) MUST /// be the same type. pub trait RefFrom { fn ref_from(value: &T) -> Self; } #[derive(Serialize, Deserialize)] pub struct GeneralBlockOptions where T: Display, { pub element: HtmlElement, pub class_list: String, pub id: String, pub attributes: HashMap, pub sub_options: T, } impl GeneralBlockOptions { pub fn boxed(self) -> GeneralBlockOptions> { GeneralBlockOptions { element: self.element, class_list: self.class_list, id: self.id, attributes: self.attributes, sub_options: Box::new(self.sub_options), } } pub fn boxed_cloned + 'static>( &self, ) -> GeneralBlockOptions> { let x: F = F::ref_from(&self.sub_options); GeneralBlockOptions { element: self.element.clone(), class_list: self.class_list.clone(), id: self.id.clone(), attributes: self.attributes.clone(), sub_options: Box::new(x), } } } impl Display for GeneralBlockOptions { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&format!( "class=\"{} {}\" {} id={} {}", self.class_list, self.sub_options.to_string(), { let mut attrs = String::new(); for (k, v) in &self.attributes { attrs.push_str(&format!("{k}=\"{v}\"")); } attrs }, self.id, if self.element == HtmlElement::Image { "/" } else { "" } )) } } #[derive(Clone, Serialize, Deserialize)] pub struct EmptyBlockOptions; impl Display for EmptyBlockOptions { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("") } } impl RefFrom for EmptyBlockOptions { fn ref_from(value: &EmptyBlockOptions) -> Self { value.to_owned() } } #[derive(Clone, Serialize, Deserialize)] pub struct FlexibleBlockOptions { pub gap: FlexibleBlockGap, pub direction: FlexibleBlockDirection, pub wrap: bool, pub collapse: bool, } impl Display for FlexibleBlockOptions { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&format!( "flex {} {} {} {}", self.gap, self.direction, if self.wrap { "flex-wrap" } else { "" }, if self.collapse { "flex-collapse" } else { "" } )) } } impl RefFrom for FlexibleBlockOptions { fn ref_from(value: &FlexibleBlockOptions) -> Self { value.to_owned() } } #[derive(Clone, Serialize, Deserialize)] pub enum FlexibleBlockGap { Tight, Comfortable, Spacious, Large, } impl Display for FlexibleBlockGap { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::Tight => "gap-1", Self::Comfortable => "gap-2", Self::Spacious => "gap-3", Self::Large => "gap-4", }) } } #[derive(Clone, Serialize, Deserialize)] pub enum FlexibleBlockDirection { Row, Column, } impl Display for FlexibleBlockDirection { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::Row => "flex-row", Self::Column => "flex-col", }) } } #[derive(Clone, Serialize, Deserialize)] pub struct MarkdownBlockOptions { pub content: String, } impl Display for MarkdownBlockOptions { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("") } } impl RefFrom for MarkdownBlockOptions { fn ref_from(value: &MarkdownBlockOptions) -> Self { value.to_owned() } } #[derive(Clone, Serialize, Deserialize)] pub struct TimelineBlockOptions { pub timeline: DefaultTimelineChoice, } impl Display for TimelineBlockOptions { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("w-full flex flex-col gap-2\" ui_ident=\"io_data_load") } } impl RefFrom for TimelineBlockOptions { fn ref_from(value: &TimelineBlockOptions) -> Self { value.to_owned() } }