feat: edit watching and watched status

This commit is contained in:
2026-10-17 01:43:50 +02:00
parent bb0a3e58ce
commit 08e4a62228
5 changed files with 476 additions and 42 deletions
+101 -1
View File
@@ -27,7 +27,7 @@ use crate::{
keys::{Action, Context},
miruro_browser::{self, Episode, Media, Request, Response, Worker},
ui,
video_state::{CollectionKind, State},
video_state::{CollectionKind, State, WatchStatus},
youtube::{PlayerLaunch, Video},
};
@@ -615,6 +615,23 @@ impl Browser {
Err(error) => self.fail(error),
}
}
fn set_watch_status(&mut self, status: WatchStatus) {
let Some(video) = self.screen().selected_video() else {
return;
};
let result = self.state.set_watch_status(&video, status);
self.rebuild_collections();
match result {
Ok(()) => {
self.status = match status {
WatchStatus::Watching => "Marked episode as watching.",
WatchStatus::Watched => "Marked episode as watched.",
}
.into();
}
Err(error) => self.fail(error),
}
}
fn remove(&mut self, bulk: bool) {
let Content::Collection { kind, videos, .. } = &self.screen().content else {
return;
@@ -811,6 +828,8 @@ impl Browser {
Some(Action::WatchingBrowse) => self.collection(CollectionKind::Watching),
Some(Action::WatchLaterSave) => self.save(CollectionKind::WatchLater),
Some(Action::SavedVideoSave) => self.save(CollectionKind::Saved),
Some(Action::WatchStatusWatching) => self.set_watch_status(WatchStatus::Watching),
Some(Action::WatchStatusWatched) => self.set_watch_status(WatchStatus::Watched),
Some(Action::CollectionRemoveSelected) => self.remove(false),
Some(Action::CollectionRemoveBulk) => self.remove(true),
Some(Action::WatchLaterFilter) => {
@@ -875,6 +894,8 @@ impl Browser {
bindings.extend([
(Action::WatchLaterSave, "Save episode to watch later"),
(Action::SavedVideoSave, "Save episode"),
(Action::WatchStatusWatching, "Mark episode as watching"),
(Action::WatchStatusWatched, "Mark episode as watched"),
]);
}
if let Content::Collection { kind, .. } = self.screen().content {
@@ -934,6 +955,8 @@ impl Browser {
&[
(Action::InfoHide, "close"),
(Action::Help, "help"),
(Action::WatchStatusWatching, "mark watching"),
(Action::WatchStatusWatched, "mark watched"),
(Action::DescriptionNext, "down"),
(Action::DescriptionPrevious, "up"),
(Action::DescriptionPageNext, "page down"),
@@ -946,6 +969,8 @@ impl Browser {
(Action::Help, "help"),
(Action::VideoPlay, "play"),
(Action::InfoOverview, "info"),
(Action::WatchStatusWatching, "mark watching"),
(Action::WatchStatusWatched, "mark watched"),
(Action::CollectionRemoveSelected, "remove"),
(Action::CollectionRemoveBulk, "remove all"),
(Action::WatchLaterFilter, "filter"),
@@ -968,6 +993,8 @@ impl Browser {
},
),
(Action::InfoOverview, "info"),
(Action::WatchStatusWatching, "mark watching"),
(Action::WatchStatusWatched, "mark watched"),
(Action::ViewPrevious, "back"),
(Action::WatchLaterBrowse, "later"),
(Action::SavedVideoBrowse, "saved"),
@@ -1636,6 +1663,79 @@ mod tests {
assert!(sources(&directory).is_empty());
}
#[test]
fn manual_watch_status_refreshes_filters_and_preserves_other_providers() {
let directory = Directory::new();
let mut browser = directory.browser(Some("anime"));
catalog_results(&mut browser);
browser.key(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE));
assert!(browser.state.videos(CollectionKind::Watching).is_empty());
assert!(!browser.bindings().iter().any(|(action, _)| {
matches!(
action,
Action::WatchStatusWatching | Action::WatchStatusWatched
)
}));
enter_series(&mut browser);
let episode = browser.screen().selected_video().unwrap();
let mut youtube = episode.clone();
youtube.id = "jNQXAC9IVRw".into();
youtube.channel_id = Some("UC4QobU6STFB0P71PMvOGN5A".into());
youtube.uploader_id = Some("@jawed".into());
browser
.state
.save(CollectionKind::Watching, &youtube)
.unwrap();
browser.save(CollectionKind::WatchLater);
browser.description_focus = true;
browser.key(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE));
assert!(
browser
.state
.contains(CollectionKind::Watching, &episode.id)
);
assert!(!browser.state.is_watched(&episode.id));
browser.description_focus = false;
browser.collection(CollectionKind::Watching);
browser.open();
assert!(browser.pending.is_some());
browser.key(KeyEvent::new(KeyCode::Char('M'), KeyModifiers::NONE));
assert!(browser.state.is_watched(&episode.id));
assert_eq!(browser.screen().len(), 0);
assert!(browser.pending.is_none());
browser.collection(CollectionKind::WatchLater);
browser.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
assert_eq!(browser.screen().len(), 0);
browser.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
assert_eq!(browser.screen().len(), 1);
browser.key(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE));
assert_eq!(browser.screen().len(), 0);
assert!(!browser.state.is_watched(&episode.id));
assert!(
browser
.state
.contains(CollectionKind::Watching, &episode.id)
);
assert!(
browser
.state
.contains(CollectionKind::Watching, &youtube.id)
);
assert!(!browser.state.is_watched(&youtube.id));
let state = State::open(&browser.config_path).unwrap();
assert!(state.contains(CollectionKind::Watching, &episode.id));
assert!(state.contains(CollectionKind::WatchLater, &episode.id));
let overrides: toml_edit::DocumentMut =
"[keys.video.status]\nwatched = 'alt+m'\n".parse().unwrap();
browser.config.keys = crate::keys::Bindings::from_toml(overrides.get("keys")).unwrap();
browser.collection(CollectionKind::Watching);
browser.key(KeyEvent::new(KeyCode::Char('M'), KeyModifiers::NONE));
assert!(!browser.state.is_watched(&episode.id));
browser.key(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::ALT));
assert!(browser.state.is_watched(&episode.id));
assert!(state.contains(CollectionKind::Watching, &youtube.id));
}
#[test]
fn offline_collections_and_filtered_bulk_removal_never_touch_youtube() {
let directory = Directory::new();