From 46ddb10a7a7a8c96a6ac302789c719d714bd084c Mon Sep 17 00:00:00 2001 From: salah0eldin Date: Tue, 6 Jan 2026 21:31:09 +0200 Subject: [PATCH] added feature for multiple selection of videos, showing their duration sum on top of the info of the shown file --- yazi-actor/src/mgr/spot.rs | 5 ++- yazi-core/src/spot/spot.rs | 4 +- yazi-plugin/preset/plugins/video.lua | 55 +++++++++++++++++++++++++--- yazi-plugin/src/isolate/spot.rs | 9 +++++ 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/yazi-actor/src/mgr/spot.rs b/yazi-actor/src/mgr/spot.rs index eafe6e69..9c86fbc9 100644 --- a/yazi-actor/src/mgr/spot.rs +++ b/yazi-actor/src/mgr/spot.rs @@ -20,13 +20,16 @@ impl Actor for Spot { // self.active_mut().spot.reset(); // } + // Collect selected files + let selected: Vec<_> = cx.tab().selected.values().cloned().collect(); + if let Some(skip) = opt.skip { cx.tab_mut().spot.skip = skip; } else if !cx.tab().spot.same_url(&hovered.url) { cx.tab_mut().spot.skip = 0; } - cx.tab_mut().spot.go(hovered, mime); + cx.tab_mut().spot.go(hovered, mime, selected); succ!(); } } diff --git a/yazi-core/src/spot/spot.rs b/yazi-core/src/spot/spot.rs index 34034168..e410f807 100644 --- a/yazi-core/src/spot/spot.rs +++ b/yazi-core/src/spot/spot.rs @@ -15,7 +15,7 @@ pub struct Spot { } impl Spot { - pub fn go(&mut self, file: File, mime: Symbol) { + pub fn go(&mut self, file: File, mime: Symbol, selected: Vec) { if mime.is_empty() { return; // Wait till mimetype is resolved to avoid flickering } else if self.same_lock(&file, &mime) { @@ -27,7 +27,7 @@ impl Spot { }; self.abort(); - self.ct = Some(isolate::spot(&spotter.run, file, mime, self.skip)); + self.ct = Some(isolate::spot(&spotter.run, file, mime, self.skip, selected)); } pub fn visible(&self) -> bool { self.lock.is_some() } diff --git a/yazi-plugin/preset/plugins/video.lua b/yazi-plugin/preset/plugins/video.lua index 627f7e80..f85dc92a 100644 --- a/yazi-plugin/preset/plugins/video.lua +++ b/yazi-plugin/preset/plugins/video.lua @@ -101,10 +101,57 @@ function M:spot(job) end function M:spot_base(job) + local rows = {} + + -- Check if there are multiple selected files + if job.selected and #job.selected > 1 then + -- Multi-file selection: show sum at the top + local total_dur = 0 + local video_count = 0 + + -- Start all ffprobe commands concurrently + local commands = {} + for i, file in ipairs(job.selected) do + commands[i] = Command("ffprobe") + :arg { "-v", "quiet", "-select_streams", "v", "-show_entries", "format=duration", "-of", "json=c=1", tostring(file.path) } + :stdout(Command.PIPED) + :spawn() + end + + -- Collect results + for i, child in ipairs(commands) do + local output, err = child:wait_with_output() + if output and output.status.success then + local t = ya.json_decode(output.stdout) + if t and type(t) == "table" and t.format and t.format.duration then + total_dur = total_dur + t.format.duration + video_count = video_count + 1 + end + end + end + + -- Format total duration + local hours = math.floor(total_dur / 3600) + local mins = math.floor((total_dur % 3600) / 60) + local secs = math.floor(total_dur % 60) + + local duration_str + if hours > 0 then + duration_str = string.format("%d:%02d:%02d", hours, mins, secs) + else + duration_str = string.format("%d:%02d", mins, secs) + end + + rows[#rows + 1] = ui.Row({ string.format("# Videos (%d)", video_count) }):style(ui.Style():fg("green")) + rows[#rows + 1] = ui.Row { " Duration:", duration_str } + rows[#rows + 1] = ui.Row {} -- Empty row separator + end + + -- Show detailed info for the current hovered file local meta, err = self.list_meta(job.file.path, "format=duration:stream=codec_name,codec_type,width,height") if not meta then ya.err(tostring(err)) - return {} + return rows -- Return at least the selection info if available end local dur = meta.format.duration or 0 @@ -119,10 +166,8 @@ function M:spot_base(job) duration_str = string.format("%d:%02d", mins, secs) end - local rows = { - ui.Row({ "Video" }):style(ui.Style():fg("green")), - ui.Row { " Duration:", duration_str }, - } + rows[#rows + 1] = ui.Row({ "Video" }):style(ui.Style():fg("green")) + rows[#rows + 1] = ui.Row { " Duration:", duration_str } for i, s in ipairs(meta.streams) do if s.codec_type == "video" then diff --git a/yazi-plugin/src/isolate/spot.rs b/yazi-plugin/src/isolate/spot.rs index 0b1fa111..099c2a16 100644 --- a/yazi-plugin/src/isolate/spot.rs +++ b/yazi-plugin/src/isolate/spot.rs @@ -16,6 +16,7 @@ pub fn spot( file: yazi_fs::File, mime: Symbol, skip: usize, + selected: Vec, ) -> CancellationToken { let ct = CancellationToken::new(); let (ct1, ct2) = (ct.clone(), ct.clone()); @@ -37,12 +38,20 @@ pub fn spot( )?; let plugin = LOADER.load_once(&lua, &cmd.name)?; + + // Convert selected URLs to Lua table + let selected_table = lua.create_table()?; + for (i, url) in selected.iter().enumerate() { + selected_table.set(i + 1, yazi_binding::Url::new(url.clone()))?; + } + let job = lua.create_table_from([ ("id", Id(IDS.next()).into_lua(&lua)?), ("args", Sendable::args_to_table_ref(&lua, &cmd.args)?.into_lua(&lua)?), ("file", File::new(file).into_lua(&lua)?), ("mime", mime.into_lua(&lua)?), ("skip", skip.into_lua(&lua)?), + ("selected", selected_table.into_lua(&lua)?), ])?; if ct2.is_cancelled() { Ok(()) } else { plugin.call_async_method("spot", job).await }