Add mark as read button

This commit is contained in:
Greg Shuflin 2025-02-05 01:01:10 -08:00
parent 70a13235ac
commit 086f92903b
4 changed files with 67 additions and 11 deletions

View File

@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "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": { "describe": {
"columns": [ "columns": [
{ {
@ -42,6 +42,11 @@
"name": "link", "name": "link",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
},
{
"name": "marked_read: Option<chrono::DateTime<Utc>>",
"ordinal": 8,
"type_info": "Datetime"
} }
], ],
"parameters": { "parameters": {
@ -55,8 +60,9 @@
true, true,
false, false,
true, true,
true,
true true
] ]
}, },
"hash": "aa1176c799d37727714cf346eb8a16ba561dc7a1b9b95992c38248c2102d6621" "hash": "7698fc853218a67cf22338697be3b504220e8fbf845e32945273c7e5cd579152"
} }

View File

@ -31,6 +31,7 @@ struct Entry {
summary: String, summary: String,
content: Option<feed_rs::model::Content>, content: Option<feed_rs::model::Content>,
link: Option<String>, link: Option<String>,
marked_read: Option<DateTime<Utc>>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -131,7 +132,8 @@ async fn read_entries(feed_id: &str, db: &mut SqliteConnection) -> Result<Vec<En
updated as "updated: Option<chrono::DateTime<Utc>>", updated as "updated: Option<chrono::DateTime<Utc>>",
summary, summary,
content, content,
link link,
marked_read as "marked_read: Option<chrono::DateTime<Utc>>"
FROM feed_entries FROM feed_entries
WHERE feed_id = ? WHERE feed_id = ?
ORDER BY published DESC NULLS LAST ORDER BY published DESC NULLS LAST
@ -164,6 +166,7 @@ async fn read_entries(feed_id: &str, db: &mut SqliteConnection) -> Result<Vec<En
summary: row.summary.clone(), summary: row.summary.clone(),
content, content,
link: row.link.clone(), link: row.link.clone(),
marked_read: row.marked_read.flatten(),
}) })
}) })
.collect::<Result<Vec<_>, Status>>()?; .collect::<Result<Vec<_>, Status>>()?;
@ -192,6 +195,7 @@ async fn fetch_new_entries(url: &Url) -> Result<Vec<Entry>, Status> {
summary: get(feed_entry.summary, "summary"), summary: get(feed_entry.summary, "summary"),
content: feed_entry.content, content: feed_entry.content,
link: feed_entry.links.first().map(|l| l.href.clone()), link: feed_entry.links.first().map(|l| l.href.clone()),
marked_read: None,
}) })
.collect(); .collect();
Ok(entries) Ok(entries)

View File

@ -513,19 +513,16 @@ button:disabled {
} }
.feed-entry-title { .feed-entry-title {
margin: 0 0 1rem 0; display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
} }
.feed-entry-title a { .feed-entry-title a {
color: var(--text-color); color: var(--text-color);
text-decoration: none; text-decoration: none;
font-size: 1.25rem; flex-grow: 1;
font-weight: 500;
transition: color 0.2s ease;
}
.feed-entry-title a:hover {
color: var(--primary-red);
} }
.feed-entry-meta { .feed-entry-meta {
@ -637,4 +634,19 @@ button:disabled {
letter-spacing: 0.05em; letter-spacing: 0.05em;
margin: 0; margin: 0;
border-bottom: 1px solid var(--border-color); 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;
} }

View File

@ -45,12 +45,46 @@ function renderFeedEntries(entries) {
const title = document.createElement('h2'); const title = document.createElement('h2');
title.className = 'feed-entry-title'; title.className = 'feed-entry-title';
const titleLink = document.createElement('a'); const titleLink = document.createElement('a');
titleLink.href = entry.link || '#'; titleLink.href = entry.link || '#';
titleLink.target = '_blank'; titleLink.target = '_blank';
titleLink.textContent = entry.title; titleLink.textContent = entry.title;
title.appendChild(titleLink); 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'); const meta = document.createElement('div');
meta.className = 'feed-entry-meta'; meta.className = 'feed-entry-meta';
if (entry.published) { if (entry.published) {