Compare commits
2 Commits
ab5e187212
...
086f92903b
Author | SHA1 | Date | |
---|---|---|---|
|
086f92903b | ||
|
70a13235ac |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT \n id as \"id!: String\",\n entry_id,\n title,\n published as \"published: Option<chrono::DateTime<Utc>>\",\n updated as \"updated: Option<chrono::DateTime<Utc>>\",\n summary,\n content,\n link\n FROM feed_entries \n WHERE feed_id = ?\n ORDER BY published DESC NULLS LAST\n LIMIT ?\n ",
|
||||
"query": "\n SELECT \n id as \"id!: String\",\n entry_id,\n title,\n published as \"published: Option<chrono::DateTime<Utc>>\",\n updated as \"updated: Option<chrono::DateTime<Utc>>\",\n summary,\n content,\n link,\n marked_read as \"marked_read: Option<chrono::DateTime<Utc>>\"\n FROM feed_entries \n WHERE feed_id = ?\n ORDER BY published DESC NULLS LAST\n LIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -42,6 +42,11 @@
|
||||
"name": "link",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "marked_read: Option<chrono::DateTime<Utc>>",
|
||||
"ordinal": 8,
|
||||
"type_info": "Datetime"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@ -55,8 +60,9 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "aa1176c799d37727714cf346eb8a16ba561dc7a1b9b95992c38248c2102d6621"
|
||||
"hash": "7698fc853218a67cf22338697be3b504220e8fbf845e32945273c7e5cd579152"
|
||||
}
|
12
.sqlx/query-ad2e09903009082ca746a9dc75cb04831b016d17cc7ce32fa86681431071fef6.json
generated
Normal file
12
.sqlx/query-ad2e09903009082ca746a9dc75cb04831b016d17cc7ce32fa86681431071fef6.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE feed_entries SET marked_read = ? WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ad2e09903009082ca746a9dc75cb04831b016d17cc7ce32fa86681431071fef6"
|
||||
}
|
12
.sqlx/query-c64e0927e594985ee8d2be7190c8f76fea57c4c815981673c92e709b17b9204b.json
generated
Normal file
12
.sqlx/query-c64e0927e594985ee8d2be7190c8f76fea57c4c815981673c92e709b17b9204b.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE feed_entries SET marked_read = NULL WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c64e0927e594985ee8d2be7190c8f76fea57c4c815981673c92e709b17b9204b"
|
||||
}
|
20
.sqlx/query-ce12e0b02596a4045fb2cd91d566aa633109a65afcecc92564a785282e67e193.json
generated
Normal file
20
.sqlx/query-ce12e0b02596a4045fb2cd91d566aa633109a65afcecc92564a785282e67e193.json
generated
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT 1 as found FROM feed_entries e\n JOIN feeds f ON e.feed_id = f.feed_id\n WHERE e.id = ? AND f.user_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "found",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ce12e0b02596a4045fb2cd91d566aa633109a65afcecc92564a785282e67e193"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use feed_rs;
|
||||
use url::Url;
|
||||
use tracing::{error, info};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FeedError;
|
||||
|
@ -160,6 +160,7 @@ fn rocket() -> _ {
|
||||
feeds::list_feeds,
|
||||
feeds::delete_feed,
|
||||
poll::poll_feed,
|
||||
poll::update_entry_state,
|
||||
],
|
||||
)
|
||||
.mount("/static", FileServer::from("static"))
|
||||
|
74
src/poll.rs
74
src/poll.rs
@ -4,7 +4,7 @@ use chrono::{DateTime, Duration, Utc};
|
||||
use feed_rs::model::Text;
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::uuid::Uuid;
|
||||
use rocket::serde::{self, json::Json, Serialize};
|
||||
use rocket::serde::{self, json::Json, Deserialize, Serialize};
|
||||
use rocket_db_pools::Connection;
|
||||
use sqlx::{Acquire, SqliteConnection};
|
||||
use tracing::{error, info};
|
||||
@ -31,6 +31,13 @@ struct Entry {
|
||||
summary: String,
|
||||
content: Option<feed_rs::model::Content>,
|
||||
link: Option<String>,
|
||||
marked_read: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct EntryStateUpdate {
|
||||
read: Option<bool>,
|
||||
}
|
||||
|
||||
async fn update_entry_db(
|
||||
@ -125,7 +132,8 @@ async fn read_entries(feed_id: &str, db: &mut SqliteConnection) -> Result<Vec<En
|
||||
updated as "updated: Option<chrono::DateTime<Utc>>",
|
||||
summary,
|
||||
content,
|
||||
link
|
||||
link,
|
||||
marked_read as "marked_read: Option<chrono::DateTime<Utc>>"
|
||||
FROM feed_entries
|
||||
WHERE feed_id = ?
|
||||
ORDER BY published DESC NULLS LAST
|
||||
@ -158,6 +166,7 @@ async fn read_entries(feed_id: &str, db: &mut SqliteConnection) -> Result<Vec<En
|
||||
summary: row.summary.clone(),
|
||||
content,
|
||||
link: row.link.clone(),
|
||||
marked_read: row.marked_read.flatten(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, Status>>()?;
|
||||
@ -186,6 +195,7 @@ async fn fetch_new_entries(url: &Url) -> Result<Vec<Entry>, Status> {
|
||||
summary: get(feed_entry.summary, "summary"),
|
||||
content: feed_entry.content,
|
||||
link: feed_entry.links.first().map(|l| l.href.clone()),
|
||||
marked_read: None,
|
||||
})
|
||||
.collect();
|
||||
Ok(entries)
|
||||
@ -231,3 +241,63 @@ pub async fn poll_feed(
|
||||
|
||||
Ok(Json(FeedPollResponse { count, entries }))
|
||||
}
|
||||
|
||||
#[patch("/entries/<entry_id>/state", data = "<state>")]
|
||||
pub async fn update_entry_state(
|
||||
mut db: Connection<Db>,
|
||||
entry_id: &str,
|
||||
user: AuthenticatedUser,
|
||||
state: Json<EntryStateUpdate>,
|
||||
) -> Result<Status, Status> {
|
||||
let state = state.into_inner();
|
||||
|
||||
let user_id = user.user_id.to_string();
|
||||
// Verify the entry exists and belongs to a feed owned by this user
|
||||
let exists = sqlx::query!(
|
||||
r#"
|
||||
SELECT 1 as found FROM feed_entries e
|
||||
JOIN feeds f ON e.feed_id = f.feed_id
|
||||
WHERE e.id = ? AND f.user_id = ?
|
||||
"#,
|
||||
entry_id,
|
||||
user_id,
|
||||
)
|
||||
.fetch_optional(&mut **db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error checking entry ownership: {}", e);
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
|
||||
if exists.is_none() {
|
||||
return Ok(Status::NotFound);
|
||||
}
|
||||
|
||||
// Update read state if provided
|
||||
if let Some(read) = state.read {
|
||||
let now = Utc::now();
|
||||
let result = if read {
|
||||
sqlx::query!(
|
||||
"UPDATE feed_entries SET marked_read = ? WHERE id = ?",
|
||||
now,
|
||||
entry_id
|
||||
)
|
||||
.execute(&mut **db)
|
||||
.await
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE feed_entries SET marked_read = NULL WHERE id = ?",
|
||||
entry_id
|
||||
)
|
||||
.execute(&mut **db)
|
||||
.await
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Failed to update entry read state: {}", e);
|
||||
return Ok(Status::InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Status::NoContent)
|
||||
}
|
||||
|
@ -513,19 +513,16 @@ button:disabled {
|
||||
}
|
||||
|
||||
.feed-entry-title {
|
||||
margin: 0 0 1rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feed-entry-title a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.feed-entry-title a:hover {
|
||||
color: var(--primary-red);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.feed-entry-meta {
|
||||
@ -638,3 +635,18 @@ button:disabled {
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.read-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.read-toggle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -45,12 +45,46 @@ function renderFeedEntries(entries) {
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'feed-entry-title';
|
||||
|
||||
const titleLink = document.createElement('a');
|
||||
titleLink.href = entry.link || '#';
|
||||
titleLink.target = '_blank';
|
||||
titleLink.textContent = entry.title;
|
||||
title.appendChild(titleLink);
|
||||
|
||||
const readToggle = document.createElement('button');
|
||||
readToggle.className = 'read-toggle';
|
||||
readToggle.title = entry.marked_read ? 'Mark as unread' : 'Mark as read';
|
||||
readToggle.innerHTML = entry.marked_read
|
||||
? '<i class="fa-solid fa-circle-check"></i>'
|
||||
: '<i class="fa-regular fa-circle"></i>';
|
||||
readToggle.onclick = async () => {
|
||||
try {
|
||||
const response = await fetch(`/entries/${entry.id}/state`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
read: !entry.marked_read
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
entry.marked_read = !entry.marked_read;
|
||||
readToggle.title = entry.marked_read ? 'Mark as unread' : 'Mark as read';
|
||||
readToggle.innerHTML = entry.marked_read
|
||||
? '<i class="fa-solid fa-circle-check"></i>'
|
||||
: '<i class="fa-regular fa-circle"></i>';
|
||||
} else {
|
||||
console.error('Failed to update read state:', response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update read state:', error);
|
||||
}
|
||||
};
|
||||
title.appendChild(readToggle);
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'feed-entry-meta';
|
||||
if (entry.published) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user