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}