feat: edit watching and watched status
This commit is contained in:
+89
-13
@@ -109,10 +109,10 @@
|
||||
//! Quitting cancels pending metadata/launch processes, not the separately running player.
|
||||
//! w/v save to watch later/saved videos; W/V choose combined or category scopes in searchable selectors.
|
||||
//! Alt-w/Alt-v browse the current explicit category's collection, including from collection previews.
|
||||
//! C opens combined currently watching: acknowledged started videos leave automatically once watched.
|
||||
//! C opens combined currently watching; m/M mark the selected video watching/watched, including previews.
|
||||
//! Collection views allow x/X selected/bulk removal, including previews; removal never marks watched.
|
||||
//! All three collections store offline metadata beside config.toml; channel tags determine category lists.
|
||||
//! Saved titles are green before watch-later magenta; ✓ marks acknowledged eligible playback reaching 95%.
|
||||
//! Saved titles are green before watch-later magenta; ✓ marks watched status, set manually or by eligible playback reaching 95%.
|
||||
//! In watch-later, f cycles All/Unwatched/Watched. X confirms removal of the current filtered/category subset.
|
||||
//! In saved-video views, X confirms removal of the current category/list subset; other collections and watched state stay unchanged.
|
||||
//! Configure view.collection.remove.selected and view.collection.remove.bulk; y/Enter confirms bulk, n/Esc cancels.
|
||||
@@ -151,7 +151,7 @@ use ratatui::{
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Cell, Clear, List, ListState, Paragraph, Row, Table, TableState, Wrap},
|
||||
};
|
||||
use video_state::CollectionKind;
|
||||
use video_state::{CollectionKind, WatchStatus};
|
||||
use youtube::{Job, PAGE_SIZE, Page, PlayerLaunch, Video};
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -598,12 +598,17 @@ struct KeyHints {
|
||||
impl KeyHints {
|
||||
fn new(bindings: &keys::Bindings) -> Self {
|
||||
let key = |action| bindings.label(action);
|
||||
let status = format!(
|
||||
"{} watching · {} watched",
|
||||
key(Action::WatchStatusWatching),
|
||||
key(Action::WatchStatusWatched)
|
||||
);
|
||||
Self {
|
||||
footers: [
|
||||
(
|
||||
Context::Normal,
|
||||
format!(
|
||||
"NORMAL · {}/{} select · {} play · {}/{} later · {}/{} saved · {} search · {} help · {} quit",
|
||||
"NORMAL · {}/{} select · {} play · {status} · {}/{} later · {}/{} saved · {} search · {} help · {} quit",
|
||||
key(Action::VideoNext),
|
||||
key(Action::VideoPrevious),
|
||||
key(Action::VideoPlay),
|
||||
@@ -619,7 +624,7 @@ impl KeyHints {
|
||||
(
|
||||
Context::Info,
|
||||
format!(
|
||||
"INFO · {} description · {} later · {} save · {} close · {} help",
|
||||
"INFO · {} description · {status} · {} later · {} save · {} close · {} help",
|
||||
key(Action::InfoDescription),
|
||||
key(Action::WatchLaterSave),
|
||||
key(Action::SavedVideoSave),
|
||||
@@ -630,7 +635,7 @@ impl KeyHints {
|
||||
(
|
||||
Context::Description,
|
||||
format!(
|
||||
"DESCRIPTION · {}/{} scroll · {} info · {} later · {} save · {} close · {} help",
|
||||
"DESCRIPTION · {}/{} scroll · {} info · {status} · {} later · {} save · {} close · {} help",
|
||||
key(Action::DescriptionNext),
|
||||
key(Action::DescriptionPrevious),
|
||||
key(Action::InfoOverview),
|
||||
@@ -643,7 +648,7 @@ impl KeyHints {
|
||||
(
|
||||
Context::CollectionNormal,
|
||||
format!(
|
||||
"NORMAL · {}/{} select · {} play · {}/{} later · {}/{} saved · {} remove selected · {} remove list · {} search · {} help · {} quit",
|
||||
"NORMAL · {}/{} select · {} play · {status} · {}/{} later · {}/{} saved · {} remove selected · {} remove list · {} search · {} help · {} quit",
|
||||
key(Action::VideoNext),
|
||||
key(Action::VideoPrevious),
|
||||
key(Action::VideoPlay),
|
||||
@@ -661,7 +666,7 @@ impl KeyHints {
|
||||
(
|
||||
Context::CollectionInfo,
|
||||
format!(
|
||||
"INFO · {} description · {} later · {} save · {} remove selected · {} remove list · {} close · {} help",
|
||||
"INFO · {} description · {status} · {} later · {} save · {} remove selected · {} remove list · {} close · {} help",
|
||||
key(Action::InfoDescription),
|
||||
key(Action::WatchLaterSave),
|
||||
key(Action::SavedVideoSave),
|
||||
@@ -674,7 +679,7 @@ impl KeyHints {
|
||||
(
|
||||
Context::CollectionDescription,
|
||||
format!(
|
||||
"DESCRIPTION · {}/{} scroll · {} info · {} later · {} save · {} remove selected · {} remove list · {} close · {} help",
|
||||
"DESCRIPTION · {}/{} scroll · {} info · {status} · {} later · {} save · {} remove selected · {} remove list · {} close · {} help",
|
||||
key(Action::DescriptionNext),
|
||||
key(Action::DescriptionPrevious),
|
||||
key(Action::InfoOverview),
|
||||
@@ -772,14 +777,14 @@ impl KeyHints {
|
||||
],
|
||||
preview: [
|
||||
format!(
|
||||
"· {} description · {} later · {} save · {} close ",
|
||||
"· {} description · {status} · {} later · {} save · {} close ",
|
||||
key(Action::InfoDescription),
|
||||
key(Action::WatchLaterSave),
|
||||
key(Action::SavedVideoSave),
|
||||
key(Action::InfoHide)
|
||||
),
|
||||
format!(
|
||||
"· {}/{} scroll · {} info · {} later · {} save · {} close ",
|
||||
"· {}/{} scroll · {} info · {status} · {} later · {} save · {} close ",
|
||||
key(Action::DescriptionNext),
|
||||
key(Action::DescriptionPrevious),
|
||||
key(Action::InfoOverview),
|
||||
@@ -788,7 +793,7 @@ impl KeyHints {
|
||||
key(Action::InfoHide)
|
||||
),
|
||||
format!(
|
||||
"· {} description · {} later · {} save · {} remove selected · {} remove list · {} close ",
|
||||
"· {} description · {status} · {} later · {} save · {} remove selected · {} remove list · {} close ",
|
||||
key(Action::InfoDescription),
|
||||
key(Action::WatchLaterSave),
|
||||
key(Action::SavedVideoSave),
|
||||
@@ -797,7 +802,7 @@ impl KeyHints {
|
||||
key(Action::InfoHide)
|
||||
),
|
||||
format!(
|
||||
"· {}/{} scroll · {} info · {} later · {} save · {} remove selected · {} remove list · {} close ",
|
||||
"· {}/{} scroll · {} info · {status} · {} later · {} save · {} remove selected · {} remove list · {} close ",
|
||||
key(Action::DescriptionNext),
|
||||
key(Action::DescriptionPrevious),
|
||||
key(Action::InfoOverview),
|
||||
@@ -1714,6 +1719,27 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_watch_status(&mut self, status: WatchStatus) {
|
||||
let view = &self.views[self.view_index];
|
||||
let Some(video) = view
|
||||
.selection
|
||||
.selected()
|
||||
.and_then(|row| view.videos.get(row))
|
||||
else {
|
||||
self.status = "Select a video first.".into();
|
||||
return;
|
||||
};
|
||||
let result = self.video_state.set_watch_status(video, status);
|
||||
self.rebuild_collections();
|
||||
self.status = match result {
|
||||
Ok(()) => match status {
|
||||
WatchStatus::Watching => "Video marked as watching.".into(),
|
||||
WatchStatus::Watched => "Video marked as watched.".into(),
|
||||
},
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn filter_watch_later(&mut self) {
|
||||
let view = &mut self.views[self.view_index];
|
||||
if !matches!(view.source, ViewSource::WatchLater(_)) {
|
||||
@@ -2837,6 +2863,8 @@ impl App {
|
||||
Some(Action::SavedVideoSave) => self.save_collection(CollectionKind::Saved),
|
||||
Some(Action::SavedVideoBrowse) => self.open_browser(BrowserKind::SavedVideos),
|
||||
Some(Action::WatchingBrowse) => self.browse_collection(CollectionKind::Watching),
|
||||
Some(Action::WatchStatusWatching) => self.set_watch_status(WatchStatus::Watching),
|
||||
Some(Action::WatchStatusWatched) => self.set_watch_status(WatchStatus::Watched),
|
||||
Some(Action::SavedVideoBrowseCategory) => {
|
||||
self.browse_category_collection(CollectionKind::Saved)
|
||||
}
|
||||
@@ -5739,6 +5767,54 @@ mod tests {
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_status_updates_filtered_lists_and_closes_removed_previews() {
|
||||
let mut app = app(&["tubular"]);
|
||||
let document = "[keys]\nvideo.status.watching = 'f6'\nvideo.status.watched = 'f7'\n"
|
||||
.parse::<toml_edit::DocumentMut>()
|
||||
.unwrap();
|
||||
app.apply_config(config::Config {
|
||||
keys: keys::Bindings::from_toml(document.get("keys")).unwrap(),
|
||||
..Default::default()
|
||||
});
|
||||
app.views[0].videos = youtube::search_results(
|
||||
r#"{"entries":[{"id":"jNQXAC9IVRw","title":"First","uploader_id":"@creator"},{"id":"rQ_J9WH6CGk","title":"Second","uploader_id":"@other"}]}"#,
|
||||
).unwrap().videos;
|
||||
app.views[0].loaded = true;
|
||||
for row in 0..2 {
|
||||
app.views[0].selection.select(Some(row));
|
||||
app.save_collection(CollectionKind::WatchLater);
|
||||
}
|
||||
app.browse_collection(CollectionKind::WatchLater);
|
||||
app.filter_watch_later();
|
||||
app.views[app.view_index].selection.select(Some(0));
|
||||
app.preview = Some(PreviewMode::Info);
|
||||
app.key(KeyEvent::new(KeyCode::F(7), KeyModifiers::NONE));
|
||||
assert!(app.video_state.is_watched("jNQXAC9IVRw"));
|
||||
assert_eq!(app.views[app.view_index].videos[0].id, "rQ_J9WH6CGk");
|
||||
assert_eq!(app.preview, None);
|
||||
app.filter_watch_later();
|
||||
assert_eq!(app.views[app.view_index].videos[0].id, "jNQXAC9IVRw");
|
||||
app.preview = Some(PreviewMode::Description);
|
||||
app.key(KeyEvent::new(KeyCode::F(6), KeyModifiers::NONE));
|
||||
assert!(!app.video_state.is_watched("jNQXAC9IVRw"));
|
||||
assert!(app.views[app.view_index].videos.is_empty());
|
||||
assert_eq!(app.views[app.view_index].selection.selected(), None);
|
||||
assert_eq!(app.preview, None);
|
||||
assert!(
|
||||
app.video_state
|
||||
.contains(CollectionKind::Watching, "jNQXAC9IVRw")
|
||||
);
|
||||
assert!(
|
||||
app.video_state
|
||||
.contains(CollectionKind::WatchLater, "jNQXAC9IVRw")
|
||||
);
|
||||
app.browse_collection(CollectionKind::Watching);
|
||||
app.key(KeyEvent::new(KeyCode::F(7), KeyModifiers::NONE));
|
||||
assert!(app.views[app.view_index].videos.is_empty());
|
||||
assert!(app.video_state.is_watched("jNQXAC9IVRw"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn watch_later_tracks_current_tags_filters_and_confirms_only_snapshot_removal() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
|
||||
Reference in New Issue
Block a user