fix: retain view rows until refresh results arrive
This commit is contained in:
@@ -345,7 +345,7 @@ Malformed individual entries—including invalid video IDs, missing required fie
|
||||
|
||||
Feed views sort all loaded videos newest-first using available upload dates, with undated videos last. The budget limits what has been inspected: later loads may discover newer videos and insert them above older rows, while selection and pending description requests stay attached to the same video ID. Publication labels use `YYYY-MM-DD`; `~` marks an estimated UTC date, and `Unknown` means unavailable metadata.
|
||||
|
||||
Pane titles show animated spinners while content loads. Errors retain loaded rows and stop automatic retries; `r` refreshes the current view.
|
||||
Pane titles show animated spinners while content loads. Refresh keeps the current rows and selection visible until a valid first page replaces them, including when the new result is empty. Errors retain loaded rows and stop automatic retries; `r` refreshes the current view from page one.
|
||||
|
||||
Video info shows a thumbnail, title, author/publication details, and a clipped description excerpt. Press `d` again to read the full description in the same popup. Thumbnails and full descriptions are cached in memory across selections and views: 64 full descriptions and eight inactive decoded thumbnails, plus the selected image. Caches last for the session; a successful refresh clears them. Preview loading, decoding, and image resizing run asynchronously.
|
||||
|
||||
|
||||
+69
-19
@@ -358,9 +358,8 @@ impl View {
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate pagination, retaining displayed rows until the first replacement page is valid.
|
||||
fn reset(&mut self) {
|
||||
self.videos.clear();
|
||||
self.selection = TableState::default();
|
||||
self.loaded = false;
|
||||
self.next_start = 1;
|
||||
self.has_more = false;
|
||||
@@ -378,6 +377,10 @@ impl View {
|
||||
if feed {
|
||||
self.feed_cursor.accept(&page.feed_progress)?;
|
||||
}
|
||||
if !self.loaded {
|
||||
self.videos.clear();
|
||||
self.selection = TableState::default();
|
||||
}
|
||||
if !page.videos.is_empty() {
|
||||
let selected = if feed {
|
||||
self.selection.selected().and_then(|index| {
|
||||
@@ -1355,6 +1358,8 @@ impl App {
|
||||
if query != view.submitted_query {
|
||||
view.submitted_query = query.to_owned();
|
||||
view.reset();
|
||||
view.videos.clear();
|
||||
view.selection = TableState::default();
|
||||
self.job = None;
|
||||
}
|
||||
self.switch_view(index);
|
||||
@@ -1463,6 +1468,8 @@ impl App {
|
||||
view.source
|
||||
.channels(&self.config, &self.feed_channels, &view.group_channels);
|
||||
if channels.is_empty() {
|
||||
view.videos.clear();
|
||||
view.selection = TableState::default();
|
||||
view.loaded = true;
|
||||
view.has_more = false;
|
||||
self.status = format!(
|
||||
@@ -1560,10 +1567,11 @@ impl App {
|
||||
}
|
||||
});
|
||||
let feed = self.views[self.view_index].source != ViewSource::Search;
|
||||
if let Err(error) =
|
||||
parsed.and_then(|page| self.views[self.view_index].accept_page(page, feed))
|
||||
{
|
||||
self.views[self.view_index].page_error = Some(error);
|
||||
let replacing = !self.views[self.view_index].loaded;
|
||||
match parsed.and_then(|page| self.views[self.view_index].accept_page(page, feed)) {
|
||||
Ok(()) if replacing => self.close_preview(),
|
||||
Err(error) => self.views[self.view_index].page_error = Some(error),
|
||||
_ => {}
|
||||
}
|
||||
self.status = self.view_status();
|
||||
}
|
||||
@@ -4563,7 +4571,7 @@ mod tests {
|
||||
app.apply_config(config::load(&app.config_path).unwrap());
|
||||
let view = &app.views[app.view_index];
|
||||
assert_eq!(view.source, group);
|
||||
assert!(view.videos.is_empty());
|
||||
assert_eq!(view.videos[0].title, "Cached");
|
||||
assert!(!view.loaded);
|
||||
assert_eq!(view.next_start, 1);
|
||||
assert_eq!(view.group_channels.len(), 1);
|
||||
@@ -8301,6 +8309,55 @@ tags.cancel = "z"
|
||||
assert!(app.views[1].prefetch_requested);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_keeps_rows_until_a_valid_replacement_arrives() {
|
||||
let mut app = app(&["tubular"]);
|
||||
app.view_index = 1;
|
||||
// Duplicate URLs fail request creation before any extractor is spawned.
|
||||
app.feed_channels = vec!["fixture".into(), "fixture".into()];
|
||||
finish_feed_fixture(
|
||||
&mut app,
|
||||
r#"{"entries":[{"id":"jNQXAC9IVRw","title":"Old"},{"id":"rQ_J9WH6CGk","title":"Selected"}]}
|
||||
{"entries":[]}"#,
|
||||
false,
|
||||
);
|
||||
app.views[1].selection.select(Some(1));
|
||||
app.load_view();
|
||||
assert_eq!(app.views[1].videos[1].title, "Selected");
|
||||
assert_eq!(app.views[1].selection.selected(), Some(1));
|
||||
assert!(app.views[1].page_error.is_some());
|
||||
assert_eq!(app.views[1].next_start, 1);
|
||||
|
||||
app.finish_page(Ok("not JSON".into()));
|
||||
assert_eq!(app.views[1].videos[1].title, "Selected");
|
||||
assert_eq!(app.views[1].selection.selected(), Some(1));
|
||||
|
||||
finish_feed_fixture(
|
||||
&mut app,
|
||||
r#"{"entries":[{"id":"jNQXAC9IVRw","title":"Updated"}]}
|
||||
{"entries":[]}"#,
|
||||
true,
|
||||
);
|
||||
assert_eq!(app.views[1].videos.len(), 1);
|
||||
assert_eq!(app.views[1].videos[0].title, "Updated");
|
||||
assert_eq!(app.views[1].selection.selected(), Some(0));
|
||||
assert_eq!(app.views[1].next_start, PAGE_SIZE + 1);
|
||||
finish_feed_fixture(
|
||||
&mut app,
|
||||
r#"{"entries":[{"id":"rQ_J9WH6CGk","title":"Next page"}]}
|
||||
{"entries":[]}"#,
|
||||
false,
|
||||
);
|
||||
assert_eq!(app.views[1].videos[0].title, "Updated");
|
||||
assert_eq!(app.views[1].videos[1].title, "Next page");
|
||||
|
||||
app.load_view();
|
||||
assert_eq!(app.views[1].videos[0].title, "Updated");
|
||||
finish_feed_fixture(&mut app, "{\"entries\":[]}\n{\"entries\":[]}", false);
|
||||
assert!(app.views[1].videos.is_empty());
|
||||
assert_eq!(app.views[1].selection.selected(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loaded_feed_rows_are_visible_sorted_and_keep_selection_and_description_identity() {
|
||||
let mut app = app(&["tubular"]);
|
||||
@@ -8407,7 +8464,6 @@ tags.cancel = "z"
|
||||
assert_eq!(app.views[1].feed_cursor.plan(1).unwrap(), next);
|
||||
assert!(!app.can_prefetch());
|
||||
app.views[1].reset();
|
||||
assert!(app.views[1].videos.is_empty());
|
||||
assert!(app.views[1].feed_requests.is_empty());
|
||||
assert!(!app.views[1].prefetch_requested);
|
||||
assert_eq!(app.views[1].feed_cursor.plan(1).unwrap()[0].start, 1);
|
||||
@@ -8510,7 +8566,7 @@ tags.cancel = "z"
|
||||
..Default::default()
|
||||
});
|
||||
assert!(!app.views[2].loaded);
|
||||
assert!(app.views[2].videos.is_empty());
|
||||
assert_eq!(app.views[2].videos[0].id, "jNQXAC9IVRw");
|
||||
assert!(app.views[2].feed_requests.is_empty());
|
||||
assert!(!app.views[2].prefetch_requested);
|
||||
assert_eq!(app.views[2].next_start, 1);
|
||||
@@ -8568,11 +8624,9 @@ tags.cancel = "z"
|
||||
view.feed_requests = view.feed_cursor.plan(1).unwrap();
|
||||
view.prefetch_requested = true;
|
||||
view.reset();
|
||||
assert!(view.videos.is_empty());
|
||||
assert!(view.feed_requests.is_empty());
|
||||
assert!(!view.feed_cursor.has_more());
|
||||
assert!(!view.prefetch_requested);
|
||||
assert_eq!(view.selection.selected(), None);
|
||||
assert_eq!(view.next_start, 1);
|
||||
assert!(!view.loaded);
|
||||
assert!(!view.has_more);
|
||||
@@ -8934,10 +8988,12 @@ tags.cancel = "z"
|
||||
pinned_categories: Vec::new(),
|
||||
});
|
||||
assert_eq!(app.views[app.view_index].name, "Rust");
|
||||
assert!(app.views[app.view_index].videos.is_empty());
|
||||
assert!(app.views[app.view_index].selection.selected().is_none());
|
||||
assert!(!app.views[app.view_index].loaded);
|
||||
assert!(app.job.is_none());
|
||||
app.load_page();
|
||||
assert!(app.views[app.view_index].videos.is_empty());
|
||||
assert!(app.views[app.view_index].selection.selected().is_none());
|
||||
assert!(app.views[app.view_index].loaded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -9011,8 +9067,6 @@ tags.cancel = "z"
|
||||
Some("Retained description")
|
||||
);
|
||||
assert!(!app.views[4].loaded);
|
||||
assert!(app.views[4].videos.is_empty());
|
||||
assert_eq!(app.views[4].selection.selected(), None);
|
||||
assert_eq!(app.views[4].next_start, 1);
|
||||
assert!(!app.views[4].has_more);
|
||||
assert!(app.views[4].feed_requests.is_empty());
|
||||
@@ -9050,8 +9104,6 @@ tags.cancel = "z"
|
||||
});
|
||||
assert_eq!(app.feed_channels, vec![direct, added, registered]);
|
||||
assert!(!app.views[1].loaded);
|
||||
assert!(app.views[1].videos.is_empty());
|
||||
assert_eq!(app.views[1].selection.selected(), None);
|
||||
assert_eq!(app.views[1].next_start, 1);
|
||||
assert!(!app.views[1].has_more);
|
||||
assert!(app.views[1].feed_requests.is_empty());
|
||||
@@ -9073,8 +9125,6 @@ tags.cancel = "z"
|
||||
});
|
||||
assert_eq!(app.feed_channels, vec![direct, registered]);
|
||||
assert!(!app.views[1].loaded);
|
||||
assert!(app.views[1].videos.is_empty());
|
||||
assert_eq!(app.views[1].selection.selected(), None);
|
||||
assert_eq!(app.view_index, 1);
|
||||
assert!(app.job.is_none());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user