use based::request::{RequestContext, StringResponse}; use based::ui::primitives::flex::Strategy; use based::ui::primitives::space::SpaceBetweenWidget; use based::ui::primitives::text::{Code, TextWidget}; use based::ui::{UIWidget, prelude::*}; use maud::{PreEscaped, Render, html}; use rocket::{State, get}; use pacco::pkg::{Package, Repository, arch::Architecture, find_package_by_name}; use crate::config::Config; use super::render; // TODO : API #[get("//?")] pub async fn pkg_ui( repo: &str, pkg_name: &str, ctx: RequestContext, ver: Option<&str>, ) -> Option { let repo = Repository::new(repo).unwrap(); let mut pkg = repo.get_pkg_by_name(pkg_name)?; if let Some(ver) = ver { let (version, rel) = Package::version(ver); pkg = pkg.get_version(&version); pkg.rel = rel; } let versions = pkg.versions(); let arch = pkg.arch(); let install_script = pkg.install_script(); let systemd_units = pkg.systemd_units(); let pacman_hooks = pkg.pacman_hooks(); let binaries = pkg.binaries(); let man_entries = pkg.man_entries(); let etc_entries = pkg.etc_entries(); let mut pkginfo = pkg.pkginfo(); let content = Div().vanish() .push( Flex( // Package Name Div().vanish() .push( Link(&format!("/{}", repo.name), Text(&repo.name).bold().color(&Gray::_100)._3xl() ).use_htmx() ) .push(Padding(Text("/").bold().color(&Gray::_400)).all(ScreenValue::_2)) .push( Padding(Text(&pkg.name)._3xl().bold().color(&Gray::_100)).right(ScreenValue::_2) ) .push_if(pkg.is_signed(), || { Flex( Padding( Div().vanish() .push(Span("✓")._2xl().bold().color(&Slate::_300)) .push(Span("Signed").sm().medium().color(&Slate::_300)) // TODO : Add more info: Who signed? + Public Key recv ).right(ScreenValue::_4) ).items_center().gap(ScreenValue::_2) }) .push_for_each(&arch, |arch: &Architecture| { Rounded( Padding( Background(Gray::_700, Span(&arch.to_string()).medium().sm()) ).x(ScreenValue::_3).y(ScreenValue::_1) ).size(Size::Full) }) .push(Margin( Flex( Padding( Hover(Background(Gray::_300, Nothing())).on(Background(Gray::_200, Rounded(Shadow::medium( Link(&format!("/pkg/{}/{}/{}", pkg.repo, pkg.arch.to_string(), pkg.file_name()), Paragraph( html! { svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" { path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m7-7l-7 7-7-7" {}; }; p class="ml-2 text-black" { "Download" }; } ).black().xs()))).size(Size::Large) )) ).x(ScreenValue::_2).y(ScreenValue::_2) ).items_center() ).left(ScreenValue::_4) ) ).wrap(Wrap::Wrap).gap(ScreenValue::_2).full_center().group() ) .push( Padding( Flex( Div().vanish().push( Context( FlexGrow(Strategy::Grow, Animated(Background(Gray::_800, Shadow::large(Rounded(Margin(Padding(SpaceBetween( Div() .push( Context(Text("Info").xl().semibold().color(&Gray::_300).underlined()) ) .push_some( take_out(&mut pkginfo, |x| { x.0 == "pkgdesc" }).1, |x: (String, String)| build_info(x.0, x.1) ) .push_some( take_out(&mut pkginfo, |x| { x.0 == "packager" }).1, |x: (String, String)| build_info(x.0, x.1) ) .push_some( take_out(&mut pkginfo, |x| { x.0 == "url" }).1, |x: (String, String)| build_info(x.0, x.1) ) .push_some( take_out(&mut pkginfo, |x| { x.0 == "size" }).1, |x: (String, String)| build_info(x.0, x.1) ) .push_for_each(&pkginfo, |(key, val)| build_info(key.clone(), val.clone())) ).y(ScreenValue::_2)).all(ScreenValue::_4)).all(ScreenValue::_2)).size(Size::Large))))) )).push( SpaceBetween( Padding( Background(Gray::_800, Shadow::large( Rounded( Margin( Screen::medium(FlexGrow(Strategy::NoGrow, Nothing())).on( FlexGrow(Strategy::Grow, Div().vanish() .push( Text("Versions").xl().semibold().color(&Gray::_300) ) .push( SpaceBetween( Div().vanish() .push_for_each(&versions, |version: &String| { Animated(Link(&format!("/{}/{}?ver={version}", repo.name, &pkg.name), if pkg.version.as_ref() .map(|x| *x == Package::version(&version).0).unwrap_or_default() { Text(&version).color(&Blue::_500).render() } else { Hover(Text("").color(&Blue::_400)).on( Text(&version).color(&Gray::_100)).render() } ).use_htmx()) }) ).y(ScreenValue::_1) ) )) ).all(ScreenValue::_2) ).size(Size::Large) ) ) ).all(ScreenValue::_4) ).y(ScreenValue::_2) ) ).wrap(Wrap::Wrap).group() ).top(ScreenValue::_6) ).push( Padding(Flex( SpaceBetween( Div().vanish() .push( Text("Content").xl().bold().color(&Gray::_300) ) .push( Flex( Div().vanish() .push_if(!systemd_units.is_empty(), || { InfoCard( Div().vanish() .push( CardTitle("Systemd Units") ) .push( ListElements(&systemd_units) ) ) }) .push_if(!pacman_hooks.is_empty(), || { InfoCard( Div().vanish() .push(CardTitle("Pacman Hooks")) .push( Paragraph( SpaceBetween( Div().vanish() .push_for_each(&pacman_hooks, |hook: &(String, String)| { Div().vanish() .push( Text(&hook.0).xl().semibold().color(&Gray::_300) ) .push( Padding(Rounded(Background(Gray::_700, Code(&hook.1).sm().color(&Gray::_100))).size(Size::Large)).all(ScreenValue::_4) ) }) .push( html! { @for unit in &systemd_units { li { (unit) } } }) ).y(ScreenValue::_1) ).list_style(ListStyle::Disc).color(&Gray::_300) ) ) }) .push_if(!binaries.is_empty(), || InfoCard( Div().vanish() .push( CardTitle("Binaries") ).push( ListElements(&binaries) ) ) ) .push_if(!man_entries.is_empty(), || InfoCard( Div().vanish() .push(CardTitle("Man Entries")) .push( ListElements(&man_entries) ) ) ) .push_if(!etc_entries.is_empty(), || InfoCard( Div().vanish() .push(CardTitle("Config Files")) .push( ListElements(&etc_entries) ) ) ) .push( InfoCard( Div().vanish().push( CardTitle("Package Files") ) .push( ListElements(&pkg.file_list()) ) ) ) ).group().wrap(Wrap::Wrap) ) ).y(ScreenValue::_2) ).wrap(Wrap::Wrap).group()).top(ScreenValue::_6) ).push_some(install_script.as_ref(), |install_script: &String| { SpaceBetween( Margin( Div() .push(Text("Install Script")._3xl().semibold().color(&Gray::_300).indentation(ScreenValue::_2)) .push(Rounded(Padding(Background(Gray::_700, Code(install_script.trim()) .color(&Gray::_100).sm() .indentation(ScreenValue::_0) )).all(ScreenValue::_2)).size(Size::Large)) ).top(ScreenValue::_6) ).y(ScreenValue::_4) }).render(); Some(render(content, pkg_name, ctx).await) } pub fn arch_card(a: &str, repo_name: &str, current: bool) -> PreEscaped { let url = if current { &format!("/{repo_name}") } else { &format!("/{repo_name}?arch={a}") }; let link = Link(&url, Text(&a).sm().medium().color(&Gray::_300)).use_htmx(); Rounded( Padding(if current { Background(Blue::_500, link) } else { Background(Gray::_700, link) }) .x(ScreenValue::_3) .y(ScreenValue::_1), ) .size(Size::Full) .render() } #[get("/?")] pub async fn repo_ui( repo: &str, ctx: RequestContext, arch: Option<&str>, config: &State, ) -> StringResponse { let arch = arch.map(|x| Architecture::parse(x).unwrap_or(Architecture::any)); let repo = Repository::new(repo).unwrap(); let architectures: Vec<_> = repo.arch().into_iter().map(|x| x.to_string()).collect(); let packages = if let Some(arch) = arch.clone() { repo.list_pkg_arch(arch) } else { repo.list_pkg() }; let repo_info = Margin( // Repository name and architectures Flex( Div() .vanish() .push(Text(&repo.name)._3xl().bold().color(&Gray::_100)) .push_if(config.is_mirrored_repo(&repo.name), || { Background( Blue::_500, Rounded( Margin( Padding(Text("Mirrored").sm().medium().white()) .x(ScreenValue::_3) .y(ScreenValue::_1), ) .left(ScreenValue::_4), ) .size(Size::Full), ) }) .push( Screen::medium(Margin(Nothing()).top(ScreenValue::_0)).on(Margin( Flex(Div().vanish().push_for_each(&architectures, |a: &String| { html! { @if let Some(arch) = arch.as_ref() { @if arch.to_string() == *a { (arch_card(&a, &repo.name, true)) } @else { (arch_card(&a, &repo.name, false)) } } @else { (arch_card(&a, &repo.name, false)) } } })) .group() .gap(ScreenValue::_2), ) .left(ScreenValue::_4) .top(ScreenValue::_2)), ), ) .wrap(Wrap::Wrap) .full_center() .group(), ) .bottom(ScreenValue::_6); let package_list = SpaceBetween(Div().vanish().push_for_each(&packages, |pkg| { Animated( Flex( Padding( Hover(Background(Gray::_600, Nothing())).on(Background( Gray::_700, Rounded(Shadow::medium( Link( &format!("/{}/{pkg}", repo.name), Div() .vanish() .push(Context(Sized( ScreenValue::_10, ScreenValue::_10, Rounded(Background( Blue::_500, Flex( Text( &pkg.chars() .next() .unwrap_or_default() .to_uppercase() .to_string(), ) .white() .semibold(), ) .full_center() .group(), )) .size(Size::Full), ))) .push(Context(Width( ScreenValue::fit, Text(&pkg).medium().color(&Gray::_100), ))) .push(Context( Padding( Flex( Div() .vanish() .push( Span("✓")._2xl().bold().color(&Slate::_300), ) .push( Span("Signed") .sm() .medium() .color(&Slate::_300), ), ) .items_center() .gap(ScreenValue::_2) .group(), ) .right(ScreenValue::_4), )), ) .use_htmx(), )) .size(Size::Large), )), ) .all(ScreenValue::_4), ) .items_center() .gap(ScreenValue::_4), ) })) .y(ScreenValue::_4); let content = Div().vanish().push(repo_info).push(package_list).render(); render(content, &repo.name, ctx).await } pub fn build_info(key: String, value: String) -> PreEscaped { match key.as_str() { "pkgname" => {} "xdata" => {} "arch" => {} "pkbase" => {} "pkgver" => {} "pkgdesc" => { return key_value("Description".to_string(), value); } "packager" => { return key_value("Packager".to_string(), value); } "url" => { return Flex( Div() .vanish() .push(Width(ScreenValue::_32, Span("Website: ").bold())) .push( Margin(Link(&value, Span(&value).color(&Blue::_400))).left(ScreenValue::_6), ), ) .group() .render(); } "builddate" => { let date = chrono::DateTime::from_timestamp(value.parse().unwrap(), 0).unwrap(); return key_value( "Build at".to_string(), date.format("%d.%m.%Y %H:%M").to_string(), ); } "depend" => { return pkg_list_info("Depends on", &value); } "makedepend" => { return pkg_list_info("Build Dependencies", &value); } "license" => { return key_value("License".to_string(), value); } "size" => { return key_value( "Size".to_string(), bytesize::to_string(value.parse().unwrap(), false), ); } _ => { log::warn!("Unhandled PKGINFO {key} = {value}"); } } html! {} } pub fn key_value(key: String, value: String) -> PreEscaped { Flex( Div() .vanish() .push(Width(ScreenValue::_32, Span(&format!("{key}: ")).bold())) .push(Margin(Span(&value)).left(ScreenValue::_6)), ) .items_center() .group() .render() } pub fn take_out(v: &mut Vec, f: impl Fn(&T) -> bool) -> (&mut Vec, Option) { let mut index = -1; for (i, e) in v.iter().enumerate() { if f(e) { index = i as i64; } } if index != -1 { let e = v.remove(index as usize); return (v, Some(e)); } (v, None) } pub fn find_pkg_url(pkg: &str) -> Option { if let Some(pkg) = find_package_by_name(pkg) { return Some(format!("/{}/{}", pkg.repo, pkg.name)); } None } pub fn pkg_list_info(key: &str, value: &str) -> PreEscaped { let pkgs = value.split_whitespace().map(|pkg| { if let Some(pkg_url) = find_pkg_url(pkg) { Margin(Link(&pkg_url, Text(&pkg).color(&Blue::_400)).use_htmx()).left(ScreenValue::_6) } else { Margin(Span(&pkg)).left(ScreenValue::_6) } }); html! { (Flex( Div().vanish() .push(Width(ScreenValue::_32, Span(&format!("{key}: ")).bold())) .push(Flex( html! { @for pkg in pkgs { (pkg) } } ).group().wrap(Wrap::Wrap)) ).items_center().group()) } } #[allow(non_snake_case)] pub fn ListElements(el: &[String]) -> TextWidget { Paragraph( SpaceBetween(html! { @for unit in el { li { (unit) } } }) .y(ScreenValue::_1), ) .list_style(ListStyle::Disc) .color(&Gray::_300) } #[allow(non_snake_case)] pub fn CardTitle(title: &str) -> PreEscaped { Context(Text(title).large().medium().color(&Gray::_100).underlined()) } #[allow(non_snake_case)] pub fn InfoCard(inner: T) -> SpaceBetweenWidget { SpaceBetween( Padding(Background( Gray::_800, Shadow::large( Rounded(Margin(FlexGrow(Strategy::Grow, inner)).all(ScreenValue::_2)) .size(Size::Large), ), )) .all(ScreenValue::_4), ) .y(ScreenValue::_2) }