Skip to main content

flams_router_content/
archive_views.rs

1#![allow(clippy::must_use_candidate)]
2
3use flams_backend_types::archive_json::ArchiveIndex;
4use flams_web_utils::components::{LazySubtree, Leaf, Tree, wait_and_then_fn};
5use ftml_component_utils::{Header, inject_css};
6use ftml_components::components::content::CommaSep;
7use ftml_uris::{
8    ArchiveId, DocumentUri, FtmlUri, Uri, UriPath, UriWithArchive, UriWithPath,
9    components::{UriComponentTuple, UriComponents},
10};
11use leptos::prelude::*;
12
13#[component]
14pub fn ArchiveView(comps: UriComponents) -> impl IntoView {
15    let UriComponentTuple { uri, a, p, .. } = comps.into();
16    inject_css(
17        "flams-archive-block",
18        ".flams-archive-block{ margin-left:10px;padding-top:0 !important;padding-bottom:0 !important;row-gap:0 !important;box-shadow:-3px 3px 5px -1px var(--colorBrandForeground1) !important;}",
19    );
20    wait_and_then_fn(
21        move || archive_detail(uri.clone(), a.clone(), p.clone()),
22        |a| a.into_view().into_any(),
23    )
24}
25
26#[server(prefix = "/api/backend", endpoint = "archive_detail")]
27async fn archive_detail(
28    uri: Option<Uri>,
29    a: Option<ArchiveId>,
30    p: Option<String>,
31) -> Result<ArchiveDetails, ServerFnError<String>> {
32    use flams_math_archives::source_files::SourceEntryRef;
33    use flams_math_archives::utils::path_ext::RelPath;
34    use flams_math_archives::{
35        Archive, MathArchive,
36        backend::GlobalBackend,
37        manager::{ArchiveOrGroup, ArchiveTree},
38        source_files::SourceEntry,
39    };
40    use flams_web_utils::blocking_server_fn;
41    fn convert(a: &ArchiveOrGroup, tree: &ArchiveTree) -> AorGEntry {
42        match a {
43            ArchiveOrGroup::Archive(a) => AorGEntry::Archive {
44                id: a.clone(),
45                index: tree.with_index(
46                    flams_system::settings::Settings::get().external_url(),
47                    |idx, _| idx.iter().find(|i| i.id() == a).cloned(),
48                ),
49            },
50            ArchiveOrGroup::Group(gr) => AorGEntry::Group(gr.id.clone()),
51        }
52    }
53    blocking_server_fn(move || {
54        let (id, path) = match (uri, a, p) {
55            (Some(Uri::Archive(a)), _, _) => (a.id, None),
56            (Some(Uri::Path(a)), _, _) => (a.archive_id().clone(), a.path().cloned()),
57            (None, Some(a), p) => (
58                a,
59                if let Some(p) = p {
60                    if let Ok(p) = p.parse() {
61                        Some(p)
62                    } else {
63                        return Err("Invalid path segment".to_string());
64                    }
65                } else {
66                    None
67                },
68            ),
69            _ => return Err("Invalid components".to_string()),
70        };
71        GlobalBackend.with_tree(move |tree| match tree.get_group_or_archive(&id) {
72            Some(ArchiveOrGroup::Group(gr)) if path.is_none() => Ok(ArchiveDetails::Group {
73                id,
74                children: gr.children.iter().map(|e| convert(e, tree)).collect(),
75            }),
76            Some(ArchiveOrGroup::Archive(_)) => {
77                let index = tree.with_index(
78                    flams_system::settings::Settings::get().external_url(),
79                    |idx, _| idx.iter().find(|i| i.id() == &id).cloned(),
80                );
81                let children = tree
82                    .get(&id)
83                    .and_then(|a| match a {
84                        Archive::Local(l) => Some(l.with_sources(|s| {
85                            let children = if let Some(p) = &path
86                                && let Some(SourceEntryRef::Dir(d)) = s.find(RelPath::from_path(p))
87                            {
88                                &d.children
89                            } else {
90                                &s.children
91                            };
92                            children
93                                .iter()
94                                .map(|c| match c {
95                                    SourceEntry::Dir(p) => DirOrFile::Dir(
96                                        p.relative_path.clone().expect("should be impossible"),
97                                    ),
98                                    SourceEntry::File(sf) => DirOrFile::File {
99                                        uri: DocumentUri::from_archive_relpath(
100                                            l.uri().clone(),
101                                            sf.relative_path.as_ref(),
102                                        )
103                                        .expect("should be impossible"),
104                                        name: sf.relative_path.clone(),
105                                    },
106                                })
107                                .collect()
108                        })),
109                        Archive::Ext(_, _) => None,
110                    })
111                    .unwrap_or_default();
112                Ok(ArchiveDetails::Archive {
113                    id,
114                    index,
115                    children,
116                })
117            }
118            _ => Err(format!("Archive {id} not found")),
119        })
120    })
121    .await
122}
123
124#[derive(Clone, serde::Serialize, serde::Deserialize)]
125#[allow(clippy::large_enum_variant)]
126enum ArchiveDetails {
127    Group {
128        id: ArchiveId,
129        children: Box<[AorGEntry]>,
130    },
131    Archive {
132        id: ArchiveId,
133        index: Option<ArchiveIndex>,
134        children: Box<[DirOrFile]>,
135    },
136}
137impl ArchiveDetails {
138    fn do_children(
139        self,
140    ) -> (
141        String,
142        Option<Box<str>>,
143        Option<Box<str>>,
144        Box<[Box<str>]>,
145        impl IntoView,
146    ) {
147        match self {
148            Self::Group { id, children } => (
149                id.to_string(),
150                None,
151                None,
152                Box::new([]),
153                leptos::either::Either::Left(
154                    children
155                        .into_iter()
156                        .map(AorGEntry::into_view)
157                        .collect_view(),
158                ),
159            ),
160            Self::Archive {
161                id,
162                index,
163                children,
164            } => {
165                let (teaser, logo, authors) = match index {
166                    Some(
167                        ArchiveIndex::Book {
168                            teaser,
169                            thumbnail,
170                            authors,
171                            ..
172                        }
173                        | ArchiveIndex::Paper {
174                            thumbnail,
175                            teaser,
176                            authors,
177                            ..
178                        }
179                        | ArchiveIndex::Course {
180                            teaser,
181                            thumbnail,
182                            authors,
183                            ..
184                        }
185                        | ArchiveIndex::SelfStudy {
186                            thumbnail,
187                            teaser,
188                            authors,
189                            ..
190                        },
191                    ) => (teaser, thumbnail, authors),
192                    Some(ArchiveIndex::Library {
193                        teaser, thumbnail, ..
194                    }) => (teaser, thumbnail, Box::new([]) as _),
195                    _ => (None, None, Box::new([]) as _),
196                };
197                (
198                    id.to_string(),
199                    teaser,
200                    logo,
201                    authors,
202                    leptos::either::Either::Right(view!(<Tree>{
203                        children
204                            .into_iter()
205                            .map(|d| d.into_view(id.clone()))
206                            .collect_view()
207                    }</Tree>)),
208                )
209            }
210        }
211    }
212    fn into_view(self) -> impl IntoView {
213        use flams_web_utils::components::{Layout, LayoutHeader};
214        let (id, teaser, logo, authors, children) = self.do_children();
215        view! {
216            <Layout>
217                <LayoutHeader slot><h2>{id}</h2>
218                    <div>{CommaSep("",authors.into_iter().map(|s| s.into_string())).into_view()}</div>
219                    <div>
220                        {teaser.map(|t| view!(<div inner_html=t.into_string()/>))}
221                        {logo.map(|src| view!(
222                            <div style="margin-left:auto;">
223                                <img src=src.into_string() style="max-width:150px;max-height:150px;"/>
224                            </div>
225                        ))}
226
227                    </div>
228                </LayoutHeader>
229                {children}
230            </Layout>
231        }
232    }
233}
234
235#[derive(Clone, serde::Serialize, serde::Deserialize)]
236#[allow(clippy::large_enum_variant)]
237enum AorGEntry {
238    Group(ArchiveId),
239    Archive {
240        id: ArchiveId,
241        index: Option<ArchiveIndex>,
242    },
243}
244impl AorGEntry {
245    fn into_view(self) -> impl IntoView {
246        use ftml_component_utils::LazyCollapsible;
247        use ftml_component_utils::{Block, BoldCaption, HeaderLeft, HeaderRight};
248        let (title, id, teaser, authors, logo) = match self {
249            Self::Group(id) => (id.to_string(), id, None, Vec::new(), None),
250            Self::Archive { id, index } => {
251                if let Some(i) = index {
252                    (
253                        format!("{} ({id})", i.title()),
254                        id,
255                        i.teaser().map(str::to_string),
256                        i.authors().to_vec(),
257                        i.thumbnail().map(str::to_string),
258                    )
259                } else {
260                    (id.to_string(), id, None, Vec::new(), None)
261                }
262            }
263        };
264        view! {
265            <Block class="flams-archive-block">
266                <Header slot><div style="display:flex;flex-direction:column;">
267                    <BoldCaption>{title}</BoldCaption>
268                    <div style="font-size:small;">{CommaSep("",authors.into_iter().map(str::into_string)).into_view()}</div>
269                </div></Header>
270                <HeaderLeft slot>
271                    <div inner_html=teaser style="font-size:small"/>
272                </HeaderLeft>
273                <HeaderRight slot>{logo.map(|s| view!(<img src=s style="max-width:100px;max-height:100px;"/>))}</HeaderRight>
274                <LazyCollapsible>
275                    <Header slot><span style="font-size:small;">"Contents"</span></Header>
276                    {
277                        let id = id.clone();
278                        wait_and_then_fn(
279                            move || archive_detail(None, Some(id.clone()), None),
280                            |a| a.do_children().4.into_any(),
281                        )
282                    }
283                </LazyCollapsible>
284            </Block>
285        }
286    }
287}
288
289#[derive(Clone, serde::Serialize, serde::Deserialize)]
290#[allow(clippy::large_enum_variant)]
291enum DirOrFile {
292    Dir(UriPath),
293    File { uri: DocumentUri, name: UriPath },
294}
295
296impl DirOrFile {
297    fn into_view(self, id: ArchiveId) -> impl IntoView {
298        use flams_web_utils::components::{Drawer, Trigger};
299        use ftml_component_utils::{Button, ButtonAppearance};
300        use leptos::either::Either::{Left, Right};
301        match self {
302            Self::Dir(path) => {
303                let name = path
304                    .as_ref()
305                    .rsplit_once('/')
306                    .map_or_else(|| path.to_string(), |(_, e)| e.to_string());
307                let f = move || {
308                    wait_and_then_fn(
309                        move || archive_detail(None, Some(id.clone()), Some(path.to_string())),
310                        |a| a.do_children().4.into_any(),
311                    )
312                };
313                Left(view! {<LazySubtree>
314                    <Header slot><ftml_component_utils::icons::FolderIcon/>" "{name}</Header>
315                    {
316                        (f.clone())()
317                    }
318                </LazySubtree>})
319            }
320            Self::File { uri, .. } => {
321                let name = format!(" {} ({})", uri.name, uri.language);
322                let namecl = name.clone();
323                let link = format!("/?uri={}", uri.url_encoded());
324                let comps = ftml_uris::components::DocumentUriComponents::Full(uri);
325                Right(view! {<Leaf>
326                <Drawer lazy=true>
327                    <Trigger slot>
328                        <span style="cursor:pointer;"><ftml_component_utils::icons::FileIcon/>{name}</span>
329                    </Trigger>
330                    <Header slot><a href=link target="_blank">
331                      <Button appearance=ButtonAppearance::Subtle>{namecl}</Button>
332                    </a></Header>
333                    <div style="width:min-content">
334                        <crate::components::Document doc=comps.clone()/>
335                    </div>
336                </Drawer>
337                </Leaf>})
338            }
339        }
340    }
341}