M Cargo.lock => Cargo.lock +135 -1
@@ 40,6 40,21 @@ dependencies = [
]
[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 121,6 136,22 @@ dependencies = [
]
[[package]]
+name = "async-compression"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60"
+dependencies = [
+ "brotli",
+ "flate2",
+ "futures-core",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+ "zstd",
+ "zstd-safe",
+]
+
+[[package]]
name = "async-trait"
version = "0.1.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 316,6 347,27 @@ dependencies = [
]
[[package]]
+name = "brotli"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 338,6 390,11 @@ name = "cc"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
+dependencies = [
+ "jobserver",
+ "libc",
+ "once_cell",
+]
[[package]]
name = "cfg-if"
@@ 485,6 542,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
+name = "crc32fast"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "crossbeam-queue"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 698,6 764,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
[[package]]
+name = "flate2"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
name = "flume"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 1148,6 1224,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8"
[[package]]
+name = "jobserver"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 1879,7 1964,7 @@ dependencies = [
[[package]]
name = "russet"
-version = "0.10.1"
+version = "0.11.0"
dependencies = [
"argon2",
"atom_syndication",
@@ 1901,6 1986,7 @@ dependencies = [
"tokio",
"toml",
"tower",
+ "tower-http",
"tracing",
"tracing-subscriber",
"ulid",
@@ 2684,6 2770,26 @@ dependencies = [
]
[[package]]
+name = "tower-http"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
+dependencies = [
+ "async-compression",
+ "bitflags 2.5.0",
+ "bytes",
+ "futures-core",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "http-body-util",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 3186,3 3292,31 @@ name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
+
+[[package]]
+name = "zstd"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.10+zstd.1.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
M Cargo.toml => Cargo.toml +1 -1
@@ 1,6 1,6 @@
[package]
name = "russet"
-version = "0.10.1"
+version = "0.11.0"
edition = "2021"
license = "AGPL-3.0"
M src/domain/entries.rs => src/domain/entries.rs +16 -1
@@ 2,7 2,7 @@ use chrono::{ DateTime, TimeDelta, Utc };
use chrono_tz::Tz;
use crate::domain::model::Entry;
use crate::domain::RussetDomainService;
-use crate::model::{ EntryId, Pagination, Timestamp, UserId };
+use crate::model::{ EntryId, FeedId, Pagination, Timestamp, UserId };
use crate::persistence::model::{ Entry as PersistenceEntry, UserEntry };
use crate::persistence::RussetEntryPersistenceLayer;
use crate::Result;
@@ 35,6 35,21 @@ where Persistence: RussetEntryPersistenceLayer {
.map(|entry| convert_entry(entry, Some(user_entry), Tz::UTC))?
)
}
+
+ pub async fn get_feed_entries(
+ &self,
+ user_id: &UserId,
+ feed_id: &FeedId,
+ pagination: &Pagination,
+ ) -> impl IntoIterator<Item = Result<Entry>> {
+ self.persistence
+ .get_entries_for_user_feed(user_id, feed_id, pagination)
+ .await
+ .into_iter()
+ .map(|result| result.map(|(entry, user_entry)| convert_entry(entry, user_entry, /*FIXME*/Tz::UTC)))
+ .filter(|entry| entry.as_ref().map_or_else(|_| true, |entry| !entry.tombstone))
+ .collect::<Vec<Result<Entry>>>()
+ }
}
fn convert_entry(entry: PersistenceEntry, user_entry: Option<UserEntry>, tz: Tz) -> Entry {
M src/domain/mod.rs => src/domain/mod.rs +1 -1
@@ 48,7 48,7 @@ where Persistence: std::fmt::Debug {
.field("persistence", &self.persistence)
.field("readers", &self.readers)
.field("pepper", &"<redacted>")
- .field("feed_check_interva", &self.feed_check_interval)
+ .field("feed_check_interval", &self.feed_check_interval)
.field("disable_logins", &self.disable_logins)
.finish()
}
M src/http/feed.rs => src/http/feed.rs +18 -4
@@ 1,9 1,9 @@
-use axum::extract::{ Path, State };
+use axum::extract::{ Form, Path, State };
use axum::http::StatusCode;
use axum::response::{ Html, Redirect };
-use crate::domain::model::Feed;
-use crate::http::{ AppState, AuthenticatedUser };
-use crate::model::FeedId;
+use crate::domain::model::{ Entry, Feed };
+use crate::http::{ AppState, AuthenticatedUser, PageQuery };
+use crate::model::{ FeedId, Pagination };
use crate::persistence::model::User;
use crate::persistence::RussetPersistenceLayer;
use sailfish::TemplateOnce;
@@ 12,7 12,9 @@ use sailfish::TemplateOnce;
#[template(path = "feed.stpl")]
struct FeedPageTemplate<'a> {
user: Option<&'a User>,
+ entries: &'a [Entry],
feed: &'a Feed,
+ page_num: usize,
page_title: &'a str,
relative_root: &'a str,
}
@@ 21,14 23,26 @@ pub async fn feed_page<Persistence>(
Path(feed_id): Path<FeedId>,
State(state): State<AppState<Persistence>>,
user: AuthenticatedUser<Persistence>,
+ Form(pagination): Form<PageQuery>,
) -> Html<String>
where Persistence: RussetPersistenceLayer {
+ let page_num = pagination.page_num.unwrap_or(0);
+ let page_size = pagination.page_size.unwrap_or(100);
+ let pagination = Pagination { page_num, page_size };
let feed = state.domain_service.get_feed(&feed_id).await.unwrap();
+ let entries = state.domain_service
+ .get_feed_entries(&user.user.id, &feed_id, &pagination)
+ .await
+ .into_iter()
+ .filter_map(|entry| entry.ok())
+ .collect::<Vec<Entry>>();
let page_title = format!("Feed - {}", feed.title);
Html(
FeedPageTemplate {
user: Some(&user.user),
+ entries: &entries.as_slice(),
feed: &feed,
+ page_num: pagination.page_num,
page_title: &page_title,
relative_root: "../",
}
M templates/feed.stpl => templates/feed.stpl +35 -1
@@ 1,6 1,40 @@
<% include!("head.stpl"); %>
- <p><%= feed.title %></p>
<p>Feed URL: <a href="<%- feed.url %>"><%= feed.url %></a></p>
+ <form><table>
+ <thead class="alt"><tr>
+ <th scope="col">
+ <label for="select-all">All</label>
+ <input type="checkbox" name="select-all" />
+ </th>
+ <th scope="col">Title</th>
+ <th scope="col">Date</th>
+ </tr></thead>
+ <tbody><%
+for (i, entry) in entries.iter().enumerate() {
+ let mut classes = vec![];
+ if i % 2 == 1 { classes.push("alt") };
+ if !entry.read { classes.push("unread") };
+ let classes = if classes.len() > 0 {
+ format!(" class=\"{}\"", classes.join(" "))
+ } else {
+ "".to_string()
+ };
+%>
+ <tr<%- classes %>>
+ <td class="center"><input type="checkbox" name="select-<%= entry.id.to_string() %>" /></td>
+ <td><a href="<%- relative_root %>entry/<%- entry.id.to_string() %>"><%= entry.title %></a></td>
+ <td class="center"><%= entry.article_date %></td>
+ </tr><%
+}
+%>
+ </tbody>
+ </table></form>
+ <div>Page:
+ <% if page_num > 1 { %><a href="<%- relative_root %>?page_num=0">1</a>…<% } %>
+ <% if page_num > 0 { %><a href="<%- relative_root %>?page_num=<%- page_num - 1 %>"><%- page_num %></a><% } %>
+ <%- page_num + 1 %>
+ <a href="<%- relative_root %>?page_num=<%- page_num + 1 %>"><%- page_num + 2 %></a>
+ </div>
<div style="display: flex; justify-content: center;">
<form action="<%- relative_root %>feed/<%- feed.id.to_string() %>" method="post" class="dialog">
<div class="controls">
M templates/home.stpl => templates/home.stpl +4 -1
@@ 1,7 1,10 @@
<% include!("head.stpl"); %>
<form><table>
<thead class="alt"><tr>
- <th scope="col"><input type="checkbox" name="select-all" /></th>
+ <th scope="col">
+ <label for="select-all">All</label>
+ <input type="checkbox" name="select-all" />
+ </th>
<th scope="col">Title</th>
<th scope="col">Date</th>
<th scope="col">Feed</th>