feat: copy selected video URL
This commit is contained in:
@@ -121,6 +121,7 @@ actions! {
|
||||
ViewRefresh, "view.refresh", "Refresh the current view and configuration", VIEW, ["r"];
|
||||
VideoPlay, "video.play", "Play checked videos in displayed order, or the selected video", VIEW, ["enter"];
|
||||
VideoOpen, "video.open", "Open the selected YouTube video in the default browser", VIEW, ["o"];
|
||||
VideoCopyUrl, "video.copy_url", "Copy the selected video's URL to the terminal clipboard", VIEW, ["y"];
|
||||
VideoNext, "video.select.next", "Select the next video", VIEW, ["j", "down", "ctrl+j"];
|
||||
VideoPrevious, "video.select.previous", "Select the previous video", VIEW, ["k", "up", "ctrl+k"];
|
||||
VideoFirst, "video.select.first", "Select the first video", VIEW, ["g", "home"];
|
||||
@@ -495,6 +496,7 @@ impl Bindings {
|
||||
&[VideoOpen],
|
||||
"Open the selected YouTube video in the default browser",
|
||||
),
|
||||
(&[VideoCopyUrl], "Copy the selected video's URL"),
|
||||
(
|
||||
&[WatchStatusWatching],
|
||||
"Mark selected as watching (clear watched)",
|
||||
|
||||
+10
@@ -2937,6 +2937,16 @@ impl App {
|
||||
Some(Action::VideoToggle) => self.views[self.view_index].toggle_checked(),
|
||||
Some(Action::VideoClear) => self.views[self.view_index].checked.clear(),
|
||||
Some(Action::VideoOpen) => self.open_video_in_browser(),
|
||||
Some(Action::VideoCopyUrl) => {
|
||||
let view = &self.views[self.view_index];
|
||||
if let Some(video) = view
|
||||
.selection
|
||||
.selected()
|
||||
.and_then(|index| view.videos.get(index))
|
||||
{
|
||||
self.status = ui::copy_video_url(&video.id);
|
||||
}
|
||||
}
|
||||
Some(Action::VideoNext) => {
|
||||
self.views[self.view_index].prefetch_requested = true;
|
||||
self.views[self.view_index].selection.select_next();
|
||||
|
||||
@@ -1212,6 +1212,11 @@ impl Browser {
|
||||
Some(Action::VideoPlay) => self.open(),
|
||||
Some(Action::VideoToggle) => self.screen_mut().toggle_checked(),
|
||||
Some(Action::VideoClear) => self.screen_mut().checked.clear(),
|
||||
Some(Action::VideoCopyUrl) if self.screen().playable() => {
|
||||
if let Some(id) = self.screen().selected_key() {
|
||||
self.status = ui::copy_video_url(id);
|
||||
}
|
||||
}
|
||||
Some(
|
||||
action @ (Action::VideoNext
|
||||
| Action::VideoPrevious
|
||||
@@ -1372,6 +1377,7 @@ impl Browser {
|
||||
bindings.extend([
|
||||
(Action::VideoToggle, "Toggle checked episode"),
|
||||
(Action::VideoClear, "Clear checked episodes"),
|
||||
(Action::VideoCopyUrl, "Copy selected episode URL"),
|
||||
(Action::WatchLaterSave, "Save episode to watch later"),
|
||||
(Action::SavedVideoSave, "Save episode"),
|
||||
(Action::WatchStatusWatching, "Mark episode as watching"),
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::time::Duration;
|
||||
|
||||
use ratatui::{
|
||||
Frame,
|
||||
crossterm::{clipboard::CopyToClipboard, execute},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::{Line, Span},
|
||||
@@ -12,6 +13,15 @@ pub const ACCENT: Style = Style::new().fg(Color::Cyan);
|
||||
pub const SELECTED: Style = Style::new().fg(Color::Black).bg(Color::Cyan);
|
||||
pub const SELECTION_MARKER: &str = "> ";
|
||||
|
||||
pub fn copy_video_url(id: &str) -> String {
|
||||
let result = crate::youtube::video_url(id)
|
||||
.and_then(|url| execute!(std::io::stdout(), CopyToClipboard::to_clipboard_from(url)));
|
||||
match result {
|
||||
Ok(()) => "Video URL sent to terminal clipboard.".into(),
|
||||
Err(error) => format!("Cannot copy video URL: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn browser_layout(area: Rect, header_height: u16) -> [Rect; 5] {
|
||||
Layout::vertical([
|
||||
Constraint::Length(header_height),
|
||||
|
||||
+13
-12
@@ -863,6 +863,16 @@ fn failure(errors: &Receiver<io::Result<String>>, fallback: String) -> Option<Re
|
||||
}
|
||||
}
|
||||
|
||||
pub fn video_url(id: &str) -> io::Result<String> {
|
||||
if valid_video_id(id) {
|
||||
Ok(format!("https://www.youtube.com/watch?v={id}"))
|
||||
} else {
|
||||
tubular::miruro::Identity::from_key(id)
|
||||
.and_then(|identity| identity.watch_url())
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_in_browser(id: &str) -> io::Result<Receiver<Result<(), String>>> {
|
||||
if !valid_video_id(id) {
|
||||
return Err(io::Error::new(
|
||||
@@ -876,7 +886,7 @@ pub fn open_in_browser(id: &str) -> io::Result<Receiver<Result<(), String>>> {
|
||||
"xdg-open"
|
||||
};
|
||||
let child = isolated_command(OsStr::new(opener))
|
||||
.arg(format!("https://www.youtube.com/watch?v={id}"))
|
||||
.arg(video_url(id)?)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
@@ -926,19 +936,10 @@ impl PlayerLaunch {
|
||||
"No videos selected",
|
||||
));
|
||||
}
|
||||
let playback_url = |id: &str| {
|
||||
if valid_video_id(id) {
|
||||
Ok(format!("https://www.youtube.com/watch?v={id}"))
|
||||
} else {
|
||||
tubular::miruro::Identity::from_key(id)
|
||||
.and_then(|identity| identity.watch_url())
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))
|
||||
}
|
||||
};
|
||||
let mut command = isolated_command(player);
|
||||
command.arg("--new-instance");
|
||||
if let [video] = videos {
|
||||
let url = playback_url(&video.id)?;
|
||||
let url = video_url(&video.id)?;
|
||||
let report = state.progress_file(video)?;
|
||||
crate::video_state::register_identity(&report, &video.id)?;
|
||||
command
|
||||
@@ -957,7 +958,7 @@ impl PlayerLaunch {
|
||||
"Duplicate queue identity",
|
||||
));
|
||||
}
|
||||
playback_url(&video.id)
|
||||
video_url(&video.id)
|
||||
})
|
||||
.collect::<io::Result<Vec<_>>>()?;
|
||||
let reports = state.progress_files(videos)?;
|
||||
|
||||
Reference in New Issue
Block a user