diff --git a/src/index.rs b/src/index.rs index 4a0038b210..bfb4252ff4 100644 --- a/src/index.rs +++ b/src/index.rs @@ -795,6 +795,34 @@ impl Index { self.client.get_block(&hash).into_option() } + pub(crate) fn get_collections_paginated( + &self, + page_size: usize, + page_index: usize, + ) -> Result<(Vec, bool)> { + let mut collections = self + .database + .begin_read()? + .open_multimap_table(INSCRIPTION_ID_TO_CHILDREN)? + .iter()? + .skip(page_index * page_size) + .take(page_size + 1) + .map(|result| { + result + .map(|(key, _value)| InscriptionId::load(*key.value())) + .map_err(|err| err.into()) + }) + .collect::>>()?; + + let more = collections.len() > page_size; + + if more { + collections.pop(); + } + + Ok((collections, more)) + } + #[cfg(test)] pub(crate) fn get_children_by_inscription_id( &self, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index d7c81fcc89..6dcad557e8 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -10,12 +10,12 @@ use { page_config::PageConfig, runes::Rune, templates::{ - BlockHtml, BlockJson, BlocksHtml, ChildrenHtml, ClockSvg, HomeHtml, InputHtml, - InscriptionHtml, InscriptionJson, InscriptionsBlockHtml, InscriptionsHtml, InscriptionsJson, - OutputHtml, OutputJson, PageContent, PageHtml, PreviewAudioHtml, PreviewCodeHtml, - PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, - PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, RuneHtml, RunesHtml, SatHtml, - SatJson, TransactionHtml, + BlockHtml, BlockJson, BlocksHtml, ChildrenHtml, ClockSvg, CollectionsHtml, HomeHtml, + InputHtml, InscriptionHtml, InscriptionJson, InscriptionsBlockHtml, InscriptionsHtml, + InscriptionsJson, OutputHtml, OutputJson, PageContent, PageHtml, PreviewAudioHtml, + PreviewCodeHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, + PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, RuneHtml, + RunesHtml, SatHtml, SatJson, TransactionHtml, }, }, axum::{ @@ -202,6 +202,8 @@ impl Server { get(Self::children_paginated), ) .route("/clock", get(Self::clock)) + .route("/collections", get(Self::collections)) + .route("/collections/:page", get(Self::collections_paginated)) .route("/content/:inscription_id", get(Self::content)) .route("/faq", get(Self::faq)) .route("/favicon.ico", get(Self::favicon)) @@ -1236,6 +1238,35 @@ impl Server { }) } + async fn collections( + Extension(page_config): Extension>, + Extension(index): Extension>, + ) -> ServerResult { + Self::collections_paginated(Extension(page_config), Extension(index), Path(0)).await + } + + async fn collections_paginated( + Extension(page_config): Extension>, + Extension(index): Extension>, + Path(page_index): Path, + ) -> ServerResult { + let (collections, more_collections) = index.get_collections_paginated(100, page_index)?; + + let prev = page_index.checked_sub(1); + + let next = more_collections.then_some(page_index + 1); + + Ok( + CollectionsHtml { + inscriptions: collections, + prev, + next, + } + .page(page_config) + .into_response(), + ) + } + async fn children( Extension(page_config): Extension>, Extension(index): Extension>, @@ -2675,6 +2706,8 @@ mod tests { Inscriptions \| Blocks + \| + Collections
.* @@ -3552,6 +3585,84 @@ mod tests { ); } + #[test] + fn collections_page_prev_and_next() { + let server = TestServer::new_with_regtest_with_index_sats(); + + let mut parent_ids = Vec::new(); + + for i in 0..101 { + server.mine_blocks(1); + + parent_ids.push(InscriptionId { + txid: server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }), + index: 0, + }); + } + + for (i, parent_id) in parent_ids.iter().enumerate().take(101) { + server.mine_blocks(1); + + server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[ + (i + 2, 1, 0, Default::default()), + ( + i + 102, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parent: Some(parent_id.parent_value()), + ..Default::default() + } + .to_witness(), + ), + ], + outputs: 2, + output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE], + ..Default::default() + }); + } + + server.mine_blocks(1); + + server.assert_response_regex( + "/collections", + StatusCode::OK, + r".* +

Collections

+
+ + (.*\s*){99} +
+
+prev + +
.*" + .to_string() + .unindent(), + ); + + server.assert_response_regex( + "/collections/1", + StatusCode::OK, + ".* +

Collections

+
+ +
+
+ +next +
.*" + .unindent(), + ); + } + #[test] fn responses_are_gzipped() { let server = TestServer::new(); diff --git a/src/templates.rs b/src/templates.rs index 23a26520dd..077a944dd1 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -5,6 +5,7 @@ pub(crate) use { blocks::BlocksHtml, children::ChildrenHtml, clock::ClockSvg, + collections::CollectionsHtml, home::HomeHtml, iframe::Iframe, input::InputHtml, @@ -30,6 +31,7 @@ pub mod block; mod blocks; mod children; mod clock; +pub mod collections; mod home; mod iframe; mod input; diff --git a/src/templates/collections.rs b/src/templates/collections.rs new file mode 100644 index 0000000000..fc1274b7de --- /dev/null +++ b/src/templates/collections.rs @@ -0,0 +1,65 @@ +use super::*; + +#[derive(Boilerplate)] +pub(crate) struct CollectionsHtml { + pub(crate) inscriptions: Vec, + pub(crate) prev: Option, + pub(crate) next: Option, +} + +impl PageContent for CollectionsHtml { + fn title(&self) -> String { + "Collections".into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn without_prev_and_next() { + assert_regex_match!( + CollectionsHtml { + inscriptions: vec![inscription_id(1), inscription_id(2)], + prev: None, + next: None, + }, + " +

Collections

+
+ + +
+ .* + prev + next + .* + " + .unindent() + ); + } + + #[test] + fn with_prev_and_next() { + assert_regex_match!( + CollectionsHtml { + inscriptions: vec![inscription_id(1), inscription_id(2)], + prev: Some(1), + next: Some(2), + }, + " +

Collections

+
+ + +
+ .* + + + .* + " + .unindent() + ); + } +} diff --git a/static/index.css b/static/index.css index 46cfe38c26..8a2fccecbb 100644 --- a/static/index.css +++ b/static/index.css @@ -245,15 +245,16 @@ a.mythic { } .tabs > a { - flex: 1; - padding: 1rem; color: var(--light-fg); + padding: 1rem; } -.tabs > *:nth-child(1) { +.tabs > *:first-child { + flex: 1; text-align: right; } -.tabs > *:nth-child(3) { +.tabs > *:last-child { + flex: 1; text-align: left; } diff --git a/templates/collections.html b/templates/collections.html new file mode 100644 index 0000000000..96148916c6 --- /dev/null +++ b/templates/collections.html @@ -0,0 +1,18 @@ +

Collections

+
+%% for id in &self.inscriptions { + {{Iframe::thumbnail(*id)}} +%% } +
+
+%% if let Some(prev) = self.prev { + +%% } else { +prev +%% } +%% if let Some(next) = self.next { + +%% } else { +next +%% } +
diff --git a/templates/home.html b/templates/home.html index ab0d4306ed..9593c2ea7d 100644 --- a/templates/home.html +++ b/templates/home.html @@ -2,6 +2,8 @@ Inscriptions | Blocks + | + Collections
%% for inscription in &self.inscriptions {