use based::{ page::htmx_link, request::{RequestContext, StringResponse}, }; use maud::{PreEscaped, html}; use rocket::{State, get}; use pacco::pkg::{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 { pkg = pkg.get_version(ver); } 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 mut pkginfo = pkg.pkginfo(); let content = html! { // Package Name div class="flex flex-wrap gap-2 justify-center items-center" { (htmx_link(&format!("/{}", repo.name), "text-3xl font-bold text-gray-800 dark:text-gray-100", "", html! { (repo.name) })) p class="font-bold p-2 text-gray-400" { "/" }; h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 pr-2" { (pkg.name) }; @if pkg.is_signed() { div class="flex items-center gap-2 text-slate-300 pr-4" { span class="text-2xl font-bold" { "✓" } span class="text-sm font-medium" { "Signed" } // TODO : Add more info: Who signed? + Public Key recv } } @for arch in arch { span class="px-3 py-1 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full" { (arch.to_string()) }; } a href=(format!("/pkg/{}/{}/{}", pkg.repo, pkg.arch.to_string(), pkg.file_name())) class="ml-4 inline-flex items-center px-2 py-2 bg-gray-200 text-black font-xs rounded-lg shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" { 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" { "Download" }; }; }; div class="flex flex-wrap pt-6" { div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition flex-1 m-2 grow" { h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300 underline" { "Info" }; @if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "pkgdesc" }).1 { (build_info(desc.0, desc.1)) } @if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "packager" }).1 { (build_info(desc.0, desc.1)) } @if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "url" }).1 { (build_info(desc.0, desc.1)) } @if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "size" }).1 { (build_info(desc.0, desc.1)) } @for (key, val) in pkginfo { (build_info(key, val)) }; }; div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition m-2 grow md:grow-0 md:shrink" { h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300" { "Versions" } ul class="space-y-1" { @for version in versions { li class="text-gray-800 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400 transition" { (htmx_link(&format!("/{}/{}?ver={version}", repo.name, &pkg.name), if pkg.version.as_ref().map(|x| *x == version).unwrap_or_default() { "text-blue-500" } else { "" }, "", html! { (version) })) }; } } } }; div class="flex flex-wrap pt-6" { div class="space-y-2" { h2 class="text-xl font-bold text-gray-700 dark:text-gray-300" { "Content" } div class="flex flex-wrap" { @if !systemd_units.is_empty() { div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition m-2 grow" { h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Systemd Units" } ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { @for unit in systemd_units { li { (unit) } } } } } @if !pacman_hooks.is_empty() { div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition m-2 grow" { h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Pacman Hooks" } ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { @for hook in pacman_hooks { h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300" { (hook.0) } pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg text-gray-800 dark:text-gray-100 overflow-x-auto text-sm" { (hook.1) } } } } } @if !binaries.is_empty() { div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition m-2 grow" { h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Binaries" } ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { @for binary in binaries { li { (binary) } } } } } div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition m-2 grow" { h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Package Files" } ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { @for file in pkg.file_list() { li { (file) } } } } } }; }; // Install Script @if let Some(install_script) = install_script { div class="space-y-4 pt-6" { h2 class="text-3xl font-semibold text-gray-700 dark:text-gray-300" { "Install Script" } pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg text-gray-800 dark:text-gray-100 overflow-x-auto text-sm" { (install_script) } }; } }; Some(render(content, pkg_name, ctx).await) } #[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 content = html! { // Repository name and architectures div class="flex flex-wrap items-center justify-center mb-6" { h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100" { (repo.name) }; @if config.is_mirrored_repo(&repo.name) { div class="inline-block px-3 py-1 text-sm font-medium text-white bg-blue-500 rounded-full ml-4" { "Mirrored" }; }; div class="flex gap-2 mt-2 md:mt-0 ml-4" { @for a in architectures { @if let Some(arch) = arch.as_ref() { @if arch.to_string() == a { (htmx_link(&format!("/{}", repo.name), "px-3 py-1 text-sm font-medium bg-blue-400 dark:bg-blue-500 text-gray-600 dark:text-gray-300 rounded-full", "", html! { (a) })); } @else { (htmx_link(&format!("/{}?arch={}", repo.name, a), "px-3 py-1 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full", "", html! { (a) })); } } @else { (htmx_link(&format!("/{}?arch={}", repo.name, a), "px-3 py-1 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full", "", html! { (a) })); } } } }; // Package list ul class="space-y-4" { @for pkg in packages { (htmx_link(&format!("/{}/{pkg}", repo.name), "flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg shadow-md transition hover:bg-gray-200 dark:hover:bg-gray-600", "", html! { div class="w-10 h-10 flex items-center justify-center rounded-full bg-blue-500 text-white font-semibold" { {(pkg.chars().next().unwrap_or_default().to_uppercase())} }; p class="font-medium flex-1 text-gray-800 dark:text-gray-100 max-w-fit" { (pkg) }; div class="flex items-center gap-2 text-slate-300 pr-4" { span class="text-2xl font-bold" { "✓" }; span class="text-sm font-medium" { "Signed" }; }; })) } } }; 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 html! { div class="flex" { span class="font-bold w-32" { (format!("Website: ")) }; a class="ml-2 text-blue-400" href=(value) { (value) }; }; }; } "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 { html! { div class="flex items-center" { span class="font-bold w-32" { (format!("{key}: ")) }; span class="ml-2" { (value) }; }; } } 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) { html! { a href=(pkg_url) class="ml-2 text-blue-400" { (pkg) }; } } else { html! { span class="ml-2" { (pkg) }; } } }); html! { div class="flex items-center" { span class="font-bold w-32" { (format!("{key}: ")) }; div class="flex flex-wrap" { @for pkg in pkgs { (pkg) }; }; }; } }