Release 0.5.4: remembered view, Auto theme, Currently Listening
- Remember the feed/place and tab across a reload or new visit; an unknown or unsubscribed one lands on All Subscriptions instead of the first feed alphabetically. - Add an Auto theme that follows the system's light/dark setting, and move Dark/Light/Classic/Auto into Settings as a dropdown alongside the header button's toggle. - Add Currently Listening below Popular: episodes started and not finished, across every subscribed feed, one tap to resume. Reuses the existing entries/filter machinery (Filter::InProgress) rather than a new endpoint. - Likely fix for the iOS bug where the topbar stopped responding to taps until a hard refresh (100vh -> 100dvh); unverified on a real device. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmQfE1eFPApnXWyPHBWqUA
This commit is contained in:
33
src/db.rs
33
src/db.rs
@@ -685,6 +685,10 @@ pub enum Filter {
|
||||
Unread,
|
||||
Downloaded,
|
||||
Flagged,
|
||||
/// Started (a saved playback position past the first few seconds) but not finished
|
||||
/// (`markPlayed` in the UI marks an item read at 90% played, so unread is "not finished"
|
||||
/// here too). Currently Listening, below Popular, is this filter on every feed at once.
|
||||
InProgress,
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
@@ -693,6 +697,7 @@ impl Filter {
|
||||
"unread" => Self::Unread,
|
||||
"downloaded" => Self::Downloaded,
|
||||
"flagged" => Self::Flagged,
|
||||
"in_progress" => Self::InProgress,
|
||||
_ => Self::All,
|
||||
}
|
||||
}
|
||||
@@ -708,6 +713,7 @@ impl Filter {
|
||||
"EXISTS (SELECT 1 FROM enclosures x
|
||||
WHERE x.feed_id = e.feed_id AND x.guid = e.guid AND x.path IS NOT NULL)"
|
||||
}
|
||||
Self::InProgress => "coalesce(s.position, 0) > 5 AND coalesce(s.read, 0) = 0",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1656,18 +1662,27 @@ mod tests {
|
||||
"INSERT INTO entries (feed_id, guid, title, description, first_seen) VALUES
|
||||
('f','a','Alpha dive','notes one',100),
|
||||
('f','b','Beta', 'notes two',200),
|
||||
('f','c','Gamma dive','notes three',300);
|
||||
('f','c','Gamma dive','notes three',300),
|
||||
('f','d','Delta', 'notes four',400),
|
||||
('f','e','Epsilon', 'notes five',500),
|
||||
('f','g','Gimel', 'notes six',600);
|
||||
INSERT INTO enclosures (id, feed_id, guid, url, path, state) VALUES
|
||||
(1,'f','b','u1','/tmp/b','done');
|
||||
-- Read and starred belong to a person now, so say which one.
|
||||
INSERT INTO users (id, name, is_admin) VALUES (7,'reader',1);
|
||||
INSERT INTO entry_state (user_id, feed_id, guid, read, flagged) VALUES
|
||||
(7,'f','b',1,0),
|
||||
(7,'f','c',1,1);",
|
||||
INSERT INTO entry_state (user_id, feed_id, guid, read, flagged, position) VALUES
|
||||
(7,'f','b',1,0,0),
|
||||
(7,'f','c',1,1,0),
|
||||
-- Started and not finished: this is Currently Listening.
|
||||
(7,'f','d',0,0,42),
|
||||
-- Already finished: not Currently Listening, however far it got.
|
||||
(7,'f','e',1,0,42),
|
||||
-- Barely touched (opened, closed within seconds): not Currently Listening.
|
||||
(7,'f','g',0,0,3);",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for f in [Filter::All, Filter::Unread, Filter::Downloaded, Filter::Flagged] {
|
||||
for f in [Filter::All, Filter::Unread, Filter::Downloaded, Filter::Flagged, Filter::InProgress] {
|
||||
// Both paths must run without erroring, and agree with each other.
|
||||
let order = order_sql("published", "desc");
|
||||
let rows = db.entries_in(7, Some("f"), f, None, 0, 50, &order).unwrap();
|
||||
@@ -1679,13 +1694,17 @@ mod tests {
|
||||
assert_eq!(rows.len() as i64, n, "{f:?} with search disagrees");
|
||||
}
|
||||
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::All, None).unwrap(), 3);
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::Unread, None).unwrap(), 1);
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::All, None).unwrap(), 6);
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::Unread, None).unwrap(), 3);
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::Downloaded, None).unwrap(), 1);
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::Flagged, None).unwrap(), 1);
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::All, Some("dive")).unwrap(), 2);
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::All, Some("NOTES two")).unwrap(), 1,
|
||||
"search is case-insensitive and covers the description");
|
||||
|
||||
// Currently Listening: started, not finished, and not just an accidental tap.
|
||||
let listening = db.entries_in(7, Some("f"), Filter::InProgress, None, 0, 50, &order_sql("published", "desc")).unwrap();
|
||||
assert_eq!(listening.iter().map(|e| e.guid.as_str()).collect::<Vec<_>>(), ["d"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user