From c83d0a9fc009e888b7243692e77b756a2a92fbe8 Mon Sep 17 00:00:00 2001 From: trisua Date: Wed, 2 Jul 2025 17:08:40 -0400 Subject: [PATCH] add: layouts types --- crates/core/src/database/auth.rs | 2 + crates/core/src/model/auth.rs | 8 + crates/core/src/model/layouts.rs | 386 +++++++++++++++++++++++++++++++ crates/core/src/model/mod.rs | 1 + sql_changes/users_layouts.sql | 2 + 5 files changed, 399 insertions(+) create mode 100644 crates/core/src/model/layouts.rs create mode 100644 sql_changes/users_layouts.sql diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index f6fb848..4038fb9 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -112,6 +112,7 @@ impl DataManager { invite_code: get!(x->21(i64)) as usize, secondary_permissions: SecondaryPermission::from_bits(get!(x->22(i32)) as u32).unwrap(), achievements: serde_json::from_str(&get!(x->23(String)).to_string()).unwrap(), + layouts: serde_json::from_str(&get!(x->24(String)).to_string()).unwrap(), } } @@ -293,6 +294,7 @@ impl DataManager { &(data.invite_code as i64), &(SecondaryPermission::DEFAULT.bits() as i32), &serde_json::to_string(&data.achievements).unwrap(), + &serde_json::to_string(&data.layouts).unwrap(), ] ); diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index bac4ae6..8c0e761 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use crate::model::layouts::CustomizablePage; + use super::{ oauth::AuthGrant, permissions::{FinePermission, SecondaryPermission}, @@ -61,6 +63,11 @@ pub struct User { /// Users collect achievements through little actions across the site. #[serde(default)] pub achievements: Vec, + /// The ID of each layout the user is using. + /// + /// Only applies if the user is a supporter. + #[serde(default)] + pub layouts: HashMap, } pub type UserConnections = @@ -319,6 +326,7 @@ impl User { invite_code: 0, secondary_permissions: SecondaryPermission::DEFAULT, achievements: Vec::new(), + layouts: HashMap::new(), } } diff --git a/crates/core/src/model/layouts.rs b/crates/core/src/model/layouts.rs new file mode 100644 index 0000000..7254d0a --- /dev/null +++ b/crates/core/src/model/layouts.rs @@ -0,0 +1,386 @@ +use std::{collections::HashMap, fmt::Display}; +use serde::{Deserialize, Serialize}; +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, +} + +/// The privacy of the layout, which controls who has the ability to view it. +#[derive(Serialize, Deserialize)] +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() + } +} diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs index 839310f..3ff8379 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -6,6 +6,7 @@ pub mod channels; pub mod communities; pub mod communities_permissions; pub mod journals; +pub mod layouts; pub mod moderation; pub mod oauth; pub mod permissions; diff --git a/sql_changes/users_layouts.sql b/sql_changes/users_layouts.sql new file mode 100644 index 0000000..0d8e489 --- /dev/null +++ b/sql_changes/users_layouts.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN layouts TEXT NOT NULL DEFAULT '{}';