diff --git a/.sqlx/query-aa1176c799d37727714cf346eb8a16ba561dc7a1b9b95992c38248c2102d6621.json b/.sqlx/query-7698fc853218a67cf22338697be3b504220e8fbf845e32945273c7e5cd579152.json similarity index 73% rename from .sqlx/query-aa1176c799d37727714cf346eb8a16ba561dc7a1b9b95992c38248c2102d6621.json rename to .sqlx/query-7698fc853218a67cf22338697be3b504220e8fbf845e32945273c7e5cd579152.json index b1bad20..f15771a 100644 --- a/.sqlx/query-aa1176c799d37727714cf346eb8a16ba561dc7a1b9b95992c38248c2102d6621.json +++ b/.sqlx/query-7698fc853218a67cf22338697be3b504220e8fbf845e32945273c7e5cd579152.json @@ -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>\",\n updated as \"updated: Option>\",\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>\",\n updated as \"updated: Option>\",\n summary,\n content,\n link,\n marked_read as \"marked_read: Option>\"\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>", + "ordinal": 8, + "type_info": "Datetime" } ], "parameters": { @@ -55,8 +60,9 @@ true, false, true, + true, true ] }, - "hash": "aa1176c799d37727714cf346eb8a16ba561dc7a1b9b95992c38248c2102d6621" + "hash": "7698fc853218a67cf22338697be3b504220e8fbf845e32945273c7e5cd579152" } diff --git a/src/poll.rs b/src/poll.rs index 0c169b9..6274d95 100644 --- a/src/poll.rs +++ b/src/poll.rs @@ -31,6 +31,7 @@ struct Entry { summary: String, content: Option, link: Option, + marked_read: Option>, } #[derive(Debug, Deserialize)] @@ -131,7 +132,8 @@ async fn read_entries(feed_id: &str, db: &mut SqliteConnection) -> Result>", summary, content, - link + link, + marked_read as "marked_read: Option>" FROM feed_entries WHERE feed_id = ? ORDER BY published DESC NULLS LAST @@ -164,6 +166,7 @@ async fn read_entries(feed_id: &str, db: &mut SqliteConnection) -> Result, Status>>()?; @@ -192,6 +195,7 @@ async fn fetch_new_entries(url: &Url) -> Result, 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) diff --git a/static/css/style.css b/static/css/style.css index 152e7d7..1651d2a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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 { @@ -637,4 +634,19 @@ button:disabled { letter-spacing: 0.05em; 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; } diff --git a/static/js/app.js b/static/js/app.js index cb58a26..a583e97 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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 + ? '' + : ''; + 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 + ? '' + : ''; + } 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) {