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.
|
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.
|
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) {
|
fn reset(&mut self) {
|
||||||
self.videos.clear();
|
|
||||||
self.selection = TableState::default();
|
|
||||||
self.loaded = false;
|
self.loaded = false;
|
||||||
self.next_start = 1;
|
self.next_start = 1;
|
||||||
self.has_more = false;
|
self.has_more = false;
|
||||||
@@ -378,6 +377,10 @@ impl View {
|
|||||||
if feed {
|
if feed {
|
||||||
self.feed_cursor.accept(&page.feed_progress)?;
|
self.feed_cursor.accept(&page.feed_progress)?;
|
||||||
}
|
}
|
||||||
|
if !self.loaded {
|
||||||
|
self.videos.clear();
|
||||||
|
self.selection = TableState::default();
|
||||||
|
}
|
||||||
if !page.videos.is_empty() {
|
if !page.videos.is_empty() {
|
||||||
let selected = if feed {
|
let selected = if feed {
|
||||||
self.selection.selected().and_then(|index| {
|
self.selection.selected().and_then(|index| {
|
||||||
@@ -1355,6 +1358,8 @@ impl App {
|
|||||||
if query != view.submitted_query {
|
if query != view.submitted_query {
|
||||||
view.submitted_query = query.to_owned();
|
view.submitted_query = query.to_owned();
|
||||||
view.reset();
|
view.reset();
|
||||||
|
view.videos.clear();
|
||||||
|
view.selection = TableState::default();
|
||||||
self.job = None;
|
self.job = None;
|
||||||
}
|
}
|
||||||
self.switch_view(index);
|
self.switch_view(index);
|
||||||
@@ -1463,6 +1468,8 @@ impl App {
|
|||||||
view.source
|
view.source
|
||||||
.channels(&self.config, &self.feed_channels, &view.group_channels);
|
.channels(&self.config, &self.feed_channels, &view.group_channels);
|
||||||
if channels.is_empty() {
|
if channels.is_empty() {
|
||||||
|
view.videos.clear();
|
||||||
|
view.selection = TableState::default();
|
||||||
view.loaded = true;
|
view.loaded = true;
|
||||||
view.has_more = false;
|
view.has_more = false;
|
||||||
self.status = format!(
|
self.status = format!(
|
||||||
@@ -1560,10 +1567,11 @@ impl App {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let feed = self.views[self.view_index].source != ViewSource::Search;
|
let feed = self.views[self.view_index].source != ViewSource::Search;
|
||||||
if let Err(error) =
|
let replacing = !self.views[self.view_index].loaded;
|
||||||
parsed.and_then(|page| self.views[self.view_index].accept_page(page, feed))
|
match parsed.and_then(|page| self.views[self.view_index].accept_page(page, feed)) {
|
||||||
{
|
Ok(()) if replacing => self.close_preview(),
|
||||||
self.views[self.view_index].page_error = Some(error);
|
Err(error) => self.views[self.view_index].page_error = Some(error),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
self.status = self.view_status();
|
self.status = self.view_status();
|
||||||
}
|
}
|
||||||
@@ -4563,7 +4571,7 @@ mod tests {
|
|||||||
app.apply_config(config::load(&app.config_path).unwrap());
|
app.apply_config(config::load(&app.config_path).unwrap());
|
||||||
let view = &app.views[app.view_index];
|
let view = &app.views[app.view_index];
|
||||||
assert_eq!(view.source, group);
|
assert_eq!(view.source, group);
|
||||||
assert!(view.videos.is_empty());
|
assert_eq!(view.videos[0].title, "Cached");
|
||||||
assert!(!view.loaded);
|
assert!(!view.loaded);
|
||||||
assert_eq!(view.next_start, 1);
|
assert_eq!(view.next_start, 1);
|
||||||
assert_eq!(view.group_channels.len(), 1);
|
assert_eq!(view.group_channels.len(), 1);
|
||||||
@@ -8301,6 +8309,55 @@ tags.cancel = "z"
|
|||||||
assert!(app.views[1].prefetch_requested);
|
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]
|
#[test]
|
||||||
fn loaded_feed_rows_are_visible_sorted_and_keep_selection_and_description_identity() {
|
fn loaded_feed_rows_are_visible_sorted_and_keep_selection_and_description_identity() {
|
||||||
let mut app = app(&["tubular"]);
|
let mut app = app(&["tubular"]);
|
||||||
@@ -8407,7 +8464,6 @@ tags.cancel = "z"
|
|||||||
assert_eq!(app.views[1].feed_cursor.plan(1).unwrap(), next);
|
assert_eq!(app.views[1].feed_cursor.plan(1).unwrap(), next);
|
||||||
assert!(!app.can_prefetch());
|
assert!(!app.can_prefetch());
|
||||||
app.views[1].reset();
|
app.views[1].reset();
|
||||||
assert!(app.views[1].videos.is_empty());
|
|
||||||
assert!(app.views[1].feed_requests.is_empty());
|
assert!(app.views[1].feed_requests.is_empty());
|
||||||
assert!(!app.views[1].prefetch_requested);
|
assert!(!app.views[1].prefetch_requested);
|
||||||
assert_eq!(app.views[1].feed_cursor.plan(1).unwrap()[0].start, 1);
|
assert_eq!(app.views[1].feed_cursor.plan(1).unwrap()[0].start, 1);
|
||||||
@@ -8510,7 +8566,7 @@ tags.cancel = "z"
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
assert!(!app.views[2].loaded);
|
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].feed_requests.is_empty());
|
||||||
assert!(!app.views[2].prefetch_requested);
|
assert!(!app.views[2].prefetch_requested);
|
||||||
assert_eq!(app.views[2].next_start, 1);
|
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.feed_requests = view.feed_cursor.plan(1).unwrap();
|
||||||
view.prefetch_requested = true;
|
view.prefetch_requested = true;
|
||||||
view.reset();
|
view.reset();
|
||||||
assert!(view.videos.is_empty());
|
|
||||||
assert!(view.feed_requests.is_empty());
|
assert!(view.feed_requests.is_empty());
|
||||||
assert!(!view.feed_cursor.has_more());
|
assert!(!view.feed_cursor.has_more());
|
||||||
assert!(!view.prefetch_requested);
|
assert!(!view.prefetch_requested);
|
||||||
assert_eq!(view.selection.selected(), None);
|
|
||||||
assert_eq!(view.next_start, 1);
|
assert_eq!(view.next_start, 1);
|
||||||
assert!(!view.loaded);
|
assert!(!view.loaded);
|
||||||
assert!(!view.has_more);
|
assert!(!view.has_more);
|
||||||
@@ -8934,10 +8988,12 @@ tags.cancel = "z"
|
|||||||
pinned_categories: Vec::new(),
|
pinned_categories: Vec::new(),
|
||||||
});
|
});
|
||||||
assert_eq!(app.views[app.view_index].name, "Rust");
|
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.views[app.view_index].loaded);
|
||||||
assert!(app.job.is_none());
|
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]
|
#[test]
|
||||||
@@ -9011,8 +9067,6 @@ tags.cancel = "z"
|
|||||||
Some("Retained description")
|
Some("Retained description")
|
||||||
);
|
);
|
||||||
assert!(!app.views[4].loaded);
|
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_eq!(app.views[4].next_start, 1);
|
||||||
assert!(!app.views[4].has_more);
|
assert!(!app.views[4].has_more);
|
||||||
assert!(app.views[4].feed_requests.is_empty());
|
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_eq!(app.feed_channels, vec![direct, added, registered]);
|
||||||
assert!(!app.views[1].loaded);
|
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_eq!(app.views[1].next_start, 1);
|
||||||
assert!(!app.views[1].has_more);
|
assert!(!app.views[1].has_more);
|
||||||
assert!(app.views[1].feed_requests.is_empty());
|
assert!(app.views[1].feed_requests.is_empty());
|
||||||
@@ -9073,8 +9125,6 @@ tags.cancel = "z"
|
|||||||
});
|
});
|
||||||
assert_eq!(app.feed_channels, vec![direct, registered]);
|
assert_eq!(app.feed_channels, vec![direct, registered]);
|
||||||
assert!(!app.views[1].loaded);
|
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_eq!(app.view_index, 1);
|
||||||
assert!(app.job.is_none());
|
assert!(app.job.is_none());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user