1use langbeam::LangFile;
2use pathbufd::PathBufD;
3use serde::{Serialize, Deserialize};
4use std::{
5 collections::HashMap,
6 sync::{LazyLock, RwLock},
7};
8
9use crate::model::{Profile, RelationshipStatus};
10use reva_axum::Template;
11use rainbeam_shared::config::Config;
12
13pub static LAYOUTS: LazyLock<RwLock<HashMap<String, String>>> =
14 LazyLock::new(|| RwLock::new(HashMap::new()));
15
16#[derive(Template)]
17#[template(path = "profile/layout_components/renderer.html")]
18pub struct RendererTemplate<'a> {
19 pub other: &'a Profile,
20 pub component: &'a LayoutComponent,
21 pub config: &'a Config,
23 pub profile: &'a Option<Box<Profile>>,
24 pub lang: &'a LangFile,
25 pub response_count: usize,
26 pub questions_count: usize,
27 pub followers_count: usize,
28 pub following_count: usize,
29 pub friends_count: usize,
30 pub is_following: bool,
31 pub is_following_you: bool,
32 pub relationship: RelationshipStatus,
33 pub lock_profile: bool,
34 pub disallow_anonymous: bool,
35 pub require_account: bool,
36 pub hide_social: bool,
37 pub is_powerful: bool, pub is_helper: bool, pub is_self: bool,
40}
41
42#[derive(Template)]
47#[template(path = "profile/layout_components/free_renderer.html")]
48pub struct FreeRendererTemplate<'a> {
49 pub other: &'a Profile,
50 pub component: &'a LayoutComponent,
51}
52
53#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
54pub enum ComponentName {
55 #[serde(alias = "empty")]
57 Empty,
58 #[serde(alias = "flex")]
60 Flex,
61 #[serde(alias = "banner")]
63 Banner,
64 #[serde(alias = "markdown")]
66 Markdown,
67 #[serde(alias = "feed")]
69 Feed,
70 #[serde(alias = "tabs")]
72 Tabs,
73 #[serde(alias = "ask")]
75 Ask,
76 #[serde(alias = "name")]
78 Name,
79 #[serde(alias = "about")]
81 About,
82 #[serde(alias = "actions")]
84 Actions,
85 #[serde(alias = "divider")]
87 Divider,
88 #[serde(alias = "style")]
90 Style,
91 #[serde(alias = "footer")]
93 Footer,
94}
95
96impl Default for ComponentName {
97 fn default() -> Self {
98 Self::Empty
99 }
100}
101
102#[derive(Serialize, Deserialize, Clone, Debug)]
104pub struct LayoutComponent {
105 #[serde(default)]
106 pub json: String,
107 #[serde(default)]
108 pub component: ComponentName,
109 #[serde(default)]
110 pub options: HashMap<String, String>,
111 #[serde(default)]
112 pub children: Vec<LayoutComponent>,
113}
114
115impl Default for LayoutComponent {
116 fn default() -> Self {
117 Self {
118 json: String::new(),
119 component: ComponentName::Empty,
120 options: HashMap::new(),
121 children: Vec::new(),
122 }
123 }
124}
125
126impl LayoutComponent {
127 pub fn from_json_file(file: &str) -> Self {
129 Self {
130 json: file.to_string(),
131 component: ComponentName::Empty,
132 options: HashMap::new(),
133 children: Vec::new(),
134 }
135 .fill()
136 }
137
138 pub fn fill(&self) -> LayoutComponent {
142 if !self.json.is_empty() {
143 let reader = match LAYOUTS.read() {
144 Ok(r) => r,
145 Err(_) => {
146 LAYOUTS.clear_poison();
147 return LayoutComponent::default();
148 }
149 };
150
151 return {
152 if let Some(l) = (*reader).get(&self.json) {
153 serde_json::from_str::<LayoutComponent>(l)
154 } else {
155 let l = match std::fs::read_to_string(PathBufD::current().extend(&[
156 ".config",
157 "layouts",
158 self.json.as_str(),
159 ])) {
160 Ok(l) => l,
161 Err(_) => return LayoutComponent::default(),
162 };
163
164 drop(reader); let mut writer = LAYOUTS.write().unwrap();
166 (*writer).insert(self.json.clone(), l.clone());
167
168 serde_json::from_str::<LayoutComponent>(&l)
169 }
170 .unwrap()
171 };
172 }
173
174 self.to_owned()
175 }
176
177 pub fn option(&self, k: &str, d: Option<String>) -> String {
179 match self.options.get(k) {
180 Some(v) => v.to_owned(),
181 None => {
182 if let Some(d) = d {
183 d
184 } else {
185 String::new()
186 }
187 }
188 }
189 }
190
191 pub fn render(&self, user: &Profile) -> String {
196 use ComponentName as T;
197
198 if !self.json.is_empty() {
200 return self.fill().render(user);
201 }
202
203 match self.component {
205 T::Flex => format!(
206 "<div class=\"flex {} {} {} {} {}\" style=\"{}\" id=\"{}\">{}</div>",
207 {
209 let direction = self.option("direction", None);
210 if !direction.is_empty() {
211 format!("flex-{direction}")
212 } else {
213 String::new()
214 }
215 },
216 {
217 let gap = self.option("gap", None);
218 if !gap.is_empty() {
219 format!("gap-{gap}")
220 } else {
221 String::new()
222 }
223 },
224 {
225 let collapse = self.option("collapse", None);
226 if !collapse.is_empty() {
227 "flex-collapse"
228 } else {
229 ""
230 }
231 },
232 {
233 let width = self.option("width", None);
234 if !width.is_empty() {
235 format!("w-{width}")
236 } else {
237 String::new()
238 }
239 },
240 self.option("class", None),
241 self.option("style", None),
242 self.option("id", None),
243 {
245 let mut children: String = String::new();
246
247 for child in &self.children {
248 children.push_str(&child.render(user));
249 }
250
251 children
252 }
253 ),
254 T::Divider => format!("<hr class=\"{}\" />", self.option("class", None)),
255 T::Markdown => format!(
256 "<div class=\"{}\">{}</div>",
257 self.option("class", None),
258 rainbeam_shared::ui::render_markdown(&self.option("text", None))
259 ),
260 T::Style => format!(
261 "<style>{}</style>",
262 self.option("data", None).replace("</", "")
263 ),
264 T::Empty => String::new(),
265 _ => format!("ComponentName::{:?}", self.component),
266 }
267 }
268
269 pub fn render_with_junk(
272 &self,
273 user: &Profile,
274 config: &Config,
276 profile: &Option<Box<Profile>>,
277 lang: &LangFile,
278 response_count: usize,
279 questions_count: usize,
280 followers_count: usize,
281 following_count: usize,
282 friends_count: usize,
283 is_following: bool,
284 is_following_you: bool,
285 relationship: RelationshipStatus,
286 lock_profile: bool,
287 disallow_anonymous: bool,
288 require_account: bool,
289 hide_social: bool,
290 is_powerful: bool,
291 is_helper: bool,
292 is_self: bool,
293 ) -> String {
294 use ComponentName as T;
295
296 if !self.json.is_empty() {
298 return self.fill().render_with_junk(
299 user,
300 config,
301 profile,
302 lang,
303 response_count,
304 questions_count,
305 followers_count,
306 following_count,
307 friends_count,
308 is_following,
309 is_following_you,
310 relationship,
311 lock_profile,
312 disallow_anonymous,
313 require_account,
314 hide_social,
315 is_powerful,
316 is_helper,
317 is_self,
318 );
319 }
320
321 match self.component {
323 T::Flex => format!(
324 "<div class=\"flex {} {} {} {} {}\" style=\"{}\" id=\"{}\">{}</div>",
325 {
327 let direction = self.option("direction", None);
328 if !direction.is_empty() {
329 format!("flex-{direction}")
330 } else {
331 String::new()
332 }
333 },
334 {
335 let gap = self.option("gap", None);
336 if !gap.is_empty() {
337 format!("gap-{gap}")
338 } else {
339 String::new()
340 }
341 },
342 {
343 let collapse = self.option("collapse", None);
344 if !collapse.is_empty() {
345 "flex-collapse"
346 } else {
347 ""
348 }
349 },
350 {
351 let width = self.option("width", None);
352 if !width.is_empty() {
353 format!("w-{width}")
354 } else {
355 String::new()
356 }
357 },
358 self.option("class", None),
359 self.option("style", None),
360 self.option("id", None),
361 {
363 let mut children: String = String::new();
364
365 for child in &self.children {
366 children.push_str(
367 &RendererTemplate {
368 other: user,
369 component: child,
370 config,
372 profile,
373 lang,
374 response_count,
375 questions_count,
376 followers_count,
377 following_count,
378 friends_count,
379 is_following,
380 is_following_you,
381 relationship: relationship.clone(),
382 lock_profile,
383 disallow_anonymous,
384 require_account,
385 hide_social,
386 is_powerful,
387 is_helper,
388 is_self,
389 }
390 .render()
391 .unwrap(),
392 );
393 }
394
395 children
396 }
397 ),
398 T::Divider => format!("<hr class=\"{}\" />", {
399 let class = self.option("class", None);
400 if !class.is_empty() {
401 class
402 } else {
403 String::new()
404 }
405 }),
406 T::Markdown => format!(
407 "<div class=\"{}\">{}</div>",
408 self.option("class", None),
409 rainbeam_shared::ui::render_markdown(&self.option("text", None))
410 ),
411 T::Style => format!(
412 "<style>{}</style>",
413 self.option("data", None).replace("</", "")
414 ),
415 T::Empty => String::new(),
416 _ => format!("ComponentName::{:?}", self.component),
417 }
418 }
419
420 pub fn render_block(&self) -> String {
429 use ComponentName as T;
430
431 match self.component {
432 T::Flex => format!(
433 "<div data-component-name=\"{:?}\" class=\"layout_editor_block flex {} {} {} {} {}\">{}</div>",
434 self.component,
435 {
437 let direction = self.option("direction", None);
438 if !direction.is_empty() {
439 format!("flex-{direction}")
440 } else {
441 String::new()
442 }
443 },
444 {
445 let gap = self.option("gap", None);
446 if !gap.is_empty() {
447 format!("gap-{gap}")
448 } else {
449 String::new()
450 }
451 },
452 {
453 let collapse = self.option("collapse", None);
454 if !collapse.is_empty() {
455 "flex-collapse"
456 } else {
457 ""
458 }
459 },
460 {
461 let width = self.option("width", None);
462 if !width.is_empty() {
463 format!("w-{width}")
464 } else {
465 String::new()
466 }
467 },
468 {
469 let mobile = self.option("mobile", None);
470 if !mobile.is_empty() {
471 format!("sm:{mobile}")
472 } else {
473 String::new()
474 }
475 },
476 {
477 let mut children: String = String::new();
478
479 for child in &self.children {
480 children.push_str(&child.render_block());
481 }
482
483 children
484 }
485 ),
486 _ => format!(
487 "<div class=\"layout_editor_block {}\" data-component-name=\"{:?}\">{:?} ({}b)</div>",
488 if self.component == T::Markdown {
489 "w-full"
490 } else {
491 ""
492 },
493 self.component, self.component, {
494 let mut size: usize = 0;
495
496 for option in &self.options {
497 size += option.0.len() + option.1.len()
498 }
499
500 size
501 }
502 ),
503 }
504 }
505
506 pub fn render_tree(&self) -> String {
508 let tag = if self.children.len() == 0 {
510 "div"
511 } else {
512 "details"
513 };
514
515 format!(
516 "<{tag} class=\"layout_editor_tree_block flex flex-col gap-2 w-full\" data-component-name=\"{:?}\">
517 <summary><b>{:?} ({}b)</b></summary>
518 {}
519 </{tag}>",
520 self.component,
521 self.component,
522 {
523 let mut size: usize = 0;
524
525 for option in &self.options {
526 size += option.0.len() + option.1.len()
527 }
528
529 size
530 },
531 {
532 let mut children: String = String::new();
533
534 for child in &self.children {
535 children.push_str(&child.render_tree());
536 }
537
538 children
539 }
540 )
541 }
542}
543
544impl AsRef<LayoutComponent> for LayoutComponent {
545 fn as_ref(&self) -> &LayoutComponent {
546 self
548 }
549}