From ea0dbf608e4c12d3aa95c12a028acd05770ebeef Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 24 Jul 2026 13:52:30 -0700 Subject: [PATCH 1/5] Add `/packages` grid view --- src/subcommand/serve.rs | 4 +-- src/subcommand/serve/route.rs | 11 +++++++- src/subcommand/serve/tests.rs | 41 ++++++++++++++++++++++++++- src/templates.rs | 6 +++- src/templates/packages.rs | 52 +++++++++++++++++++++++++++++++++++ static/packages.css | 27 ++++++++++++++++++ templates/packages.html | 33 +++++++++++++++++----- 7 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 static/packages.css diff --git a/src/subcommand/serve.rs b/src/subcommand/serve.rs index 637b2097..5fc07727 100644 --- a/src/subcommand/serve.rs +++ b/src/subcommand/serve.rs @@ -2,7 +2,7 @@ use { super::*, axum::{ Router, - extract::{Extension, Path, Request}, + extract::{Extension, Path, Query, Request}, http::{HeaderValue, Uri}, middleware::{self, Next}, response::{IntoResponse, Redirect, Response}, @@ -15,7 +15,7 @@ use { }, std::net::TcpStream, templates::{ - AudioHtml, DirectoryHtml, FilesHtml, ImageHtml, PackageHtml, PackagesHtml, VideoHtml, + AudioHtml, DirectoryHtml, FilesHtml, ImageHtml, PackageHtml, PackagesHtml, VideoHtml, View, }, tokio::{net::TcpListener, runtime, task::block_in_place}, tower_http::set_header::SetResponseHeaderLayer, diff --git a/src/subcommand/serve/route.rs b/src/subcommand/serve/route.rs index 8082af36..457e0aaf 100644 --- a/src/subcommand/serve/route.rs +++ b/src/subcommand/serve/route.rs @@ -1,5 +1,10 @@ use super::*; +#[derive(Deserialize)] +pub(crate) struct PackagesQuery { + view: Option, +} + pub(crate) async fn artwork( server: ServerExtension, fingerprint: Path, @@ -210,11 +215,15 @@ pub(crate) async fn package_item( }) } -pub(crate) async fn packages(server: ServerExtension) -> PageResult { +pub(crate) async fn packages( + server: ServerExtension, + Query(query): Query, +) -> PageResult { block_in_place(|| { Ok( PackagesHtml { packages: server.packages()?, + view: query.view.unwrap_or_default(), } .into(), ) diff --git a/src/subcommand/serve/tests.rs b/src/subcommand/serve/tests.rs index 8bb080ad..071be8d2 100644 --- a/src/subcommand/serve/tests.rs +++ b/src/subcommand/serve/tests.rs @@ -1578,6 +1578,30 @@ fn packages_empty() { .get("/packages") .assert_page(PackagesHtml { packages: Vec::new(), + view: View::List, + }) + .send(); +} + +#[test] +fn packages_grid() { + let server = TestServer::new(); + + let metadata = Metadata { + artwork: Some("foo.png".parse().unwrap()), + ..default() + }; + + let fingerprint = PackageBuilder::new() + .metadata(&metadata) + .file("foo.png", b"bar") + .upload(&server); + + server + .get("/packages?view=grid") + .assert_page(PackagesHtml { + packages: vec![(fingerprint, Some(metadata))], + view: View::Grid, }) .send(); } @@ -1598,10 +1622,22 @@ fn packages_include_creators_and_titles() { .get("/packages") .assert_page(PackagesHtml { packages: vec![(fingerprint, Some(metadata))], + view: View::List, }) .send(); } +#[test] +fn packages_invalid_view() { + TestServer::new() + .get("/packages?view=foo") + .status(StatusCode::BAD_REQUEST) + .assert_body( + "Failed to deserialize query string: view: unknown variant `foo`, expected `grid` or `list`", + ) + .send(); +} + #[test] fn packages_non_empty() { let server = TestServer::new(); @@ -1622,7 +1658,10 @@ fn packages_non_empty() { server .get("/packages") - .assert_page(PackagesHtml { packages }) + .assert_page(PackagesHtml { + packages, + view: View::List, + }) .send(); } diff --git a/src/templates.rs b/src/templates.rs index ab9dcd0c..ee241506 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -3,7 +3,11 @@ use super::*; pub use self::{directory::DirectoryHtml, package::PackageHtml, page::PageHtml}; pub(crate) use self::{ - audio::AudioHtml, files::FilesHtml, image::ImageHtml, packages::PackagesHtml, video::VideoHtml, + audio::AudioHtml, + files::FilesHtml, + image::ImageHtml, + packages::{PackagesHtml, View}, + video::VideoHtml, }; mod audio; diff --git a/src/templates/packages.rs b/src/templates/packages.rs index b958331a..c6bcd882 100644 --- a/src/templates/packages.rs +++ b/src/templates/packages.rs @@ -1,8 +1,17 @@ use super::*; +#[derive(Default, Deserialize)] +#[serde(rename_all = "lowercase")] +pub(crate) enum View { + Grid, + #[default] + List, +} + #[derive(Boilerplate)] pub(crate) struct PackagesHtml { pub(crate) packages: Vec<(Fingerprint, Option)>, + pub(crate) view: View, } impl PackagesHtml { @@ -24,7 +33,50 @@ impl PackagesHtml { } impl Page for PackagesHtml { + fn stylesheet(&self) -> Option<&'static str> { + Some("/static/packages.css") + } + fn title(&self) -> String { "packages ยท filepack".into() } } + +#[cfg(test)] +mod tests { + use {super::*, pretty_assertions::assert_eq}; + + #[test] + fn grid() { + let fingerprint = test::FINGERPRINT.parse::().unwrap(); + + let metadata = Metadata { + artwork: Some("foo.png".parse().unwrap()), + ..default() + }; + + assert_eq!( + PackagesHtml { + packages: vec![(fingerprint, Some(metadata)), (fingerprint, None)], + view: View::Grid, + } + .to_string(), + unindent(&format!( + " +

Packages

+ + ", + fingerprint = test::FINGERPRINT, + )), + ); + } +} diff --git a/static/packages.css b/static/packages.css new file mode 100644 index 00000000..f4b7cc60 --- /dev/null +++ b/static/packages.css @@ -0,0 +1,27 @@ +.grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + list-style: none; + padding-left: 0; + + a { + aspect-ratio: 1 / 1; + border-radius: 8px; + border: 1px solid #222; + display: block; + overflow: hidden; + transition: border-color 150ms; + + &:hover, &:focus-visible { + border-color: #444; + } + } + + img { + display: block; + height: 100%; + object-fit: contain; + width: 100%; + } +} diff --git a/templates/packages.html b/templates/packages.html index 6584fa10..bbfcc98e 100644 --- a/templates/packages.html +++ b/templates/packages.html @@ -1,17 +1,36 @@

Packages

+%% match self.view { +%% View::Grid => { +
    +%% for (fingerprint, metadata) in &self.packages { +
  • +%% if metadata.as_ref().is_some_and(|metadata| metadata.artwork.is_some()) { + + + +%% } else { + +%% } +
  • +%% } +
+%% } +%% View::List => { +%% } +%% } From 32e61ae8343a83a5054119627cc4d5f91c69737e Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 24 Jul 2026 14:05:57 -0700 Subject: [PATCH 2/5] Adjust --- src/lib.rs | 2 ++ src/package_view.rs | 9 +++++++++ src/subcommand/serve.rs | 7 ++++++- src/subcommand/serve/route.rs | 5 ----- src/subcommand/serve/tests.rs | 8 ++++---- src/templates.rs | 6 +----- src/templates/packages.rs | 12 ++---------- templates/packages.html | 4 ++-- 8 files changed, 26 insertions(+), 27 deletions(-) create mode 100644 src/package_view.rs diff --git a/src/lib.rs b/src/lib.rs index 606a6f9c..128bd055 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,6 +83,7 @@ use { ordinal::Ordinal, owo_colorize_ext::OwoColorizeExt, package::Package, + package_view::PackageView, path_error::PathError, percent_encode::PercentEncode, private_key_error::PrivateKeyError, @@ -323,6 +324,7 @@ mod or; mod ordinal; mod owo_colorize_ext; mod package; +mod package_view; mod page; mod path_error; mod percent_encode; diff --git a/src/package_view.rs b/src/package_view.rs new file mode 100644 index 00000000..419bc7a5 --- /dev/null +++ b/src/package_view.rs @@ -0,0 +1,9 @@ +use super::*; + +#[derive(Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) enum PackageView { + Grid, + #[default] + List, +} diff --git a/src/subcommand/serve.rs b/src/subcommand/serve.rs index 5fc07727..761d3818 100644 --- a/src/subcommand/serve.rs +++ b/src/subcommand/serve.rs @@ -15,7 +15,7 @@ use { }, std::net::TcpStream, templates::{ - AudioHtml, DirectoryHtml, FilesHtml, ImageHtml, PackageHtml, PackagesHtml, VideoHtml, View, + AudioHtml, DirectoryHtml, FilesHtml, ImageHtml, PackageHtml, PackagesHtml, VideoHtml, }, tokio::{net::TcpListener, runtime, task::block_in_place}, tower_http::set_header::SetResponseHeaderLayer, @@ -35,6 +35,11 @@ pub(crate) struct AuthConfig { pub(crate) audience: Option, } +#[derive(Deserialize)] +pub(crate) struct PackagesQuery { + view: Option, +} + pub(crate) struct RedirectConfig { destination: Url, domains: HashSet, diff --git a/src/subcommand/serve/route.rs b/src/subcommand/serve/route.rs index 457e0aaf..fcf91006 100644 --- a/src/subcommand/serve/route.rs +++ b/src/subcommand/serve/route.rs @@ -1,10 +1,5 @@ use super::*; -#[derive(Deserialize)] -pub(crate) struct PackagesQuery { - view: Option, -} - pub(crate) async fn artwork( server: ServerExtension, fingerprint: Path, diff --git a/src/subcommand/serve/tests.rs b/src/subcommand/serve/tests.rs index 071be8d2..9069fc83 100644 --- a/src/subcommand/serve/tests.rs +++ b/src/subcommand/serve/tests.rs @@ -1578,7 +1578,7 @@ fn packages_empty() { .get("/packages") .assert_page(PackagesHtml { packages: Vec::new(), - view: View::List, + view: PackageView::List, }) .send(); } @@ -1601,7 +1601,7 @@ fn packages_grid() { .get("/packages?view=grid") .assert_page(PackagesHtml { packages: vec![(fingerprint, Some(metadata))], - view: View::Grid, + view: PackageView::Grid, }) .send(); } @@ -1622,7 +1622,7 @@ fn packages_include_creators_and_titles() { .get("/packages") .assert_page(PackagesHtml { packages: vec![(fingerprint, Some(metadata))], - view: View::List, + view: PackageView::List, }) .send(); } @@ -1660,7 +1660,7 @@ fn packages_non_empty() { .get("/packages") .assert_page(PackagesHtml { packages, - view: View::List, + view: PackageView::List, }) .send(); } diff --git a/src/templates.rs b/src/templates.rs index ee241506..ab9dcd0c 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -3,11 +3,7 @@ use super::*; pub use self::{directory::DirectoryHtml, package::PackageHtml, page::PageHtml}; pub(crate) use self::{ - audio::AudioHtml, - files::FilesHtml, - image::ImageHtml, - packages::{PackagesHtml, View}, - video::VideoHtml, + audio::AudioHtml, files::FilesHtml, image::ImageHtml, packages::PackagesHtml, video::VideoHtml, }; mod audio; diff --git a/src/templates/packages.rs b/src/templates/packages.rs index c6bcd882..b0fa4ef4 100644 --- a/src/templates/packages.rs +++ b/src/templates/packages.rs @@ -1,17 +1,9 @@ use super::*; -#[derive(Default, Deserialize)] -#[serde(rename_all = "lowercase")] -pub(crate) enum View { - Grid, - #[default] - List, -} - #[derive(Boilerplate)] pub(crate) struct PackagesHtml { pub(crate) packages: Vec<(Fingerprint, Option)>, - pub(crate) view: View, + pub(crate) view: PackageView, } impl PackagesHtml { @@ -58,7 +50,7 @@ mod tests { assert_eq!( PackagesHtml { packages: vec![(fingerprint, Some(metadata)), (fingerprint, None)], - view: View::Grid, + view: PackageView::Grid, } .to_string(), unindent(&format!( diff --git a/templates/packages.html b/templates/packages.html index bbfcc98e..9957cc50 100644 --- a/templates/packages.html +++ b/templates/packages.html @@ -1,6 +1,6 @@

Packages

%% match self.view { -%% View::Grid => { +%% PackageView::Grid => {
    %% for (fingerprint, metadata) in &self.packages {
  • @@ -15,7 +15,7 @@

    Packages

    %% }
%% } -%% View::List => { +%% PackageView::List => {