Add playback speed controls & minor UI \ UX improvements
This commit is contained in:
parent
ddf43efc48
commit
8ad4f00c26
4 changed files with 167 additions and 42 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@
|
|||
/target/
|
||||
/vendor/
|
||||
/vendor.tar
|
||||
.idea/
|
||||
|
|
|
|||
4
res/icons/hicolor/16x16/apps/playback-speed-symbolic.svg
Normal file
4
res/icons/hicolor/16x16/apps/playback-speed-symbolic.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 8C6 6.96533 6.94659 6.18931 7.96116 6.39223L14.0388 7.60776C14.4676 7.69351 14.4676 8.30648 14.0388 8.39223L7.96116 9.60776C6.94659 9.81068 6 9.03467 6 8Z" fill="#232323"/>
|
||||
<path d="M11.9727 4.96582L11.9717 4.96484C11.0579 3.77097 9.61954 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13C9.58934 13 11.0568 12.1775 11.9727 11.0215C12.3575 10.4961 13.0321 10.3355 13.4795 10.6631C13.9268 10.9907 13.9775 11.6826 13.5928 12.208C12.3112 13.8352 10.232 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C10.2173 1 12.3105 2.17054 13.5928 3.7793C13.9775 4.30475 13.9268 4.99657 13.4795 5.32422C13.0321 5.65182 12.3575 5.49119 11.9727 4.96582Z" fill="#232323"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 782 B |
125
src/main.rs
125
src/main.rs
|
|
@ -54,6 +54,9 @@ const JUMP_BACKWARD_ICON: &[u8] =
|
|||
const JUMP_FORWARD_ICON: &[u8] =
|
||||
include_bytes!("../res/icons/hicolor/16x16/apps/jump-forward-10-symbolic.svg");
|
||||
|
||||
const PLAYBACK_SPEED_ICON: &[u8] =
|
||||
include_bytes!("../res/icons/hicolor/16x16/apps/playback-speed-symbolic.svg");
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
fn language_name(code: &str) -> Option<String> {
|
||||
|
|
@ -226,6 +229,7 @@ pub struct Flags {
|
|||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum DropdownKind {
|
||||
Audio,
|
||||
Speed,
|
||||
Subtitle,
|
||||
}
|
||||
|
||||
|
|
@ -312,6 +316,8 @@ pub enum Message {
|
|||
NewFrame,
|
||||
Reload,
|
||||
AbRepeat,
|
||||
VideoAreaClick,
|
||||
PlaybackSpeed(f64),
|
||||
ShowControls,
|
||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||
WindowClose,
|
||||
|
|
@ -342,6 +348,7 @@ pub struct App {
|
|||
text_codes: Vec<TextCode>,
|
||||
current_text: Option<i32>,
|
||||
ab_repeat: Option<(Option<f64>, Option<f64>)>,
|
||||
playback_speed: f64,
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
inhibit: tokio::sync::watch::Sender<bool>,
|
||||
}
|
||||
|
|
@ -456,6 +463,9 @@ impl App {
|
|||
self.set_looping_from_repeat_state();
|
||||
self.update_flags();
|
||||
self.update_mpris_meta();
|
||||
if self.playback_speed != 1.0 {
|
||||
self.apply_playback_speed();
|
||||
}
|
||||
self.update_title()
|
||||
}
|
||||
|
||||
|
|
@ -848,6 +858,22 @@ impl App {
|
|||
video.set_looping(self.flags.config_state.player_state.repeat == RepeatState::Track);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_playback_speed(&self) {
|
||||
let Some(video) = &self.video_opt else { return };
|
||||
let pipeline = video.pipeline();
|
||||
let position = gst::ClockTime::from_nseconds(video.position().as_nanos() as u64);
|
||||
if let Err(err) = pipeline.seek(
|
||||
self.playback_speed,
|
||||
gst::SeekFlags::FLUSH | gst::SeekFlags::ACCURATE,
|
||||
gst::SeekType::Set,
|
||||
position,
|
||||
gst::SeekType::None,
|
||||
gst::ClockTime::NONE,
|
||||
) {
|
||||
log::warn!("failed to set playback speed {}: {}", self.playback_speed, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
||||
|
|
@ -907,6 +933,7 @@ impl Application for App {
|
|||
text_codes: Vec::new(),
|
||||
current_text: None,
|
||||
ab_repeat: None,
|
||||
playback_speed: 1.0,
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
inhibit,
|
||||
};
|
||||
|
|
@ -1481,6 +1508,19 @@ impl Application for App {
|
|||
self.update_controls(true);
|
||||
}
|
||||
}
|
||||
Message::PlaybackSpeed(speed) => {
|
||||
self.playback_speed = speed;
|
||||
self.dropdown_opt = None;
|
||||
self.apply_playback_speed();
|
||||
self.update_controls(true);
|
||||
}
|
||||
Message::VideoAreaClick => {
|
||||
if self.dropdown_opt.is_some() {
|
||||
self.dropdown_opt = None;
|
||||
} else {
|
||||
return self.update(Message::PlayPause);
|
||||
}
|
||||
}
|
||||
Message::AbRepeat => {
|
||||
let current_opt = self.video_opt.as_ref().map(|v| v.position().as_secs_f64());
|
||||
if let Some(current) = current_opt {
|
||||
|
|
@ -1742,7 +1782,7 @@ impl Application for App {
|
|||
}
|
||||
|
||||
let mouse_area = widget::mouse_area(video_player)
|
||||
.on_press(Message::PlayPause)
|
||||
.on_press(Message::VideoAreaClick)
|
||||
.on_double_press(Message::Fullscreen);
|
||||
|
||||
let mut popover = widget::popover(mouse_area).position(widget::popover::Position::Bottom);
|
||||
|
|
@ -1779,6 +1819,61 @@ impl Application for App {
|
|||
.into(),
|
||||
);
|
||||
}
|
||||
DropdownKind::Speed => {
|
||||
for &speed in &[0.75f64, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25] {
|
||||
let s = format!("{:.2}", speed);
|
||||
let speed_str = format!(
|
||||
"{}x",
|
||||
s.trim_end_matches('0').trim_end_matches('.')
|
||||
);
|
||||
let is_active = (self.playback_speed - speed).abs() < 0.01;
|
||||
|
||||
let icon: Element<_> = if is_active {
|
||||
widget::icon::from_name("object-select-symbolic")
|
||||
.size(16)
|
||||
.into()
|
||||
} else {
|
||||
widget::container(widget::space::horizontal())
|
||||
.width(Length::Fixed(16.0))
|
||||
.into()
|
||||
};
|
||||
|
||||
let label: Element<_> = if is_active {
|
||||
widget::text(speed_str).font(font::bold()).into()
|
||||
} else {
|
||||
widget::text(speed_str).into()
|
||||
};
|
||||
|
||||
let row_content = widget::row::with_children(vec![icon, label])
|
||||
.spacing(space_xs)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
let content: Element<_> = if is_active {
|
||||
widget::container(row_content)
|
||||
.class(theme::Container::custom(|theme| {
|
||||
let accent: Color =
|
||||
theme.cosmic().accent_color().into();
|
||||
widget::container::Style {
|
||||
icon_color: Some(accent),
|
||||
text_color: Some(accent),
|
||||
..Default::default()
|
||||
}
|
||||
}))
|
||||
.into()
|
||||
} else {
|
||||
row_content.into()
|
||||
};
|
||||
|
||||
items.push(
|
||||
widget::button::custom(content)
|
||||
.width(Length::Fill)
|
||||
.padding([12.0f32, 8.0f32])
|
||||
.class(theme::Button::MenuItem)
|
||||
.on_press(Message::PlaybackSpeed(speed))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
DropdownKind::Subtitle => {
|
||||
if !self.audio_codes.is_empty() {
|
||||
items.push(widget::text::heading(fl!("audio")).into());
|
||||
|
|
@ -1807,14 +1902,20 @@ impl Application for App {
|
|||
}
|
||||
}
|
||||
|
||||
let use_padding = !matches!(dropdown, DropdownKind::Speed);
|
||||
let mut column = widget::column::with_capacity(items.len());
|
||||
for item in items {
|
||||
column = column.push(widget::container(item).padding([space_xxs, space_m]));
|
||||
column = column.push(if use_padding {
|
||||
widget::container(item).padding([space_xxs, space_m]).into()
|
||||
} else {
|
||||
item
|
||||
});
|
||||
}
|
||||
|
||||
popup_items.push(
|
||||
widget::row::with_children(vec![
|
||||
widget::space::horizontal().into(),
|
||||
widget::mouse_area(
|
||||
widget::container(column)
|
||||
.padding(1)
|
||||
//TODO: move style to libcosmic
|
||||
|
|
@ -1833,7 +1934,9 @@ impl Application for App {
|
|||
..Default::default()
|
||||
}
|
||||
}))
|
||||
.width(Length::Fixed(240.0))
|
||||
.width(Length::Fixed(240.0)),
|
||||
)
|
||||
.on_press(Message::ShowControls)
|
||||
.into(),
|
||||
])
|
||||
.into(),
|
||||
|
|
@ -1930,6 +2033,12 @@ impl Application for App {
|
|||
)
|
||||
.on_press(Message::DropdownToggle(DropdownKind::Subtitle)),
|
||||
)
|
||||
.push(
|
||||
widget::button::icon(
|
||||
widget::icon::from_svg_bytes(PLAYBACK_SPEED_ICON).symbolic(true),
|
||||
)
|
||||
.on_press(Message::DropdownToggle(DropdownKind::Speed)),
|
||||
)
|
||||
.push(
|
||||
widget::button::icon(
|
||||
widget::icon::from_name("view-fullscreen-symbolic").size(16),
|
||||
|
|
@ -1957,14 +2066,18 @@ impl Application for App {
|
|||
.on_press(Message::DropdownToggle(DropdownKind::Audio)),
|
||||
);
|
||||
popup_items.push(
|
||||
widget::mouse_area(
|
||||
widget::container(row)
|
||||
.padding([space_xxs, space_xs])
|
||||
.class(theme::Container::WindowBackground)
|
||||
.class(theme::Container::WindowBackground),
|
||||
)
|
||||
.on_press(Message::ShowControls)
|
||||
.into(),
|
||||
);
|
||||
|
||||
if self.core.is_condensed() {
|
||||
popup_items.push(
|
||||
widget::mouse_area(
|
||||
widget::container(
|
||||
widget::row::with_capacity(3)
|
||||
.align_y(Alignment::Center)
|
||||
|
|
@ -1981,7 +2094,9 @@ impl Application for App {
|
|||
),
|
||||
)
|
||||
.padding([space_xxs, space_xs])
|
||||
.class(theme::Container::WindowBackground)
|
||||
.class(theme::Container::WindowBackground),
|
||||
)
|
||||
.on_press(Message::ShowControls)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ pub fn new_video(
|
|||
.downcast::<gst::Pipeline>()
|
||||
.map_err(|_| iced_video_player::Error::Cast)
|
||||
.unwrap();
|
||||
if let Ok(scaletempo) = gst::ElementFactory::make("scaletempo").build() {
|
||||
pipeline.set_property("audio-filter", &scaletempo);
|
||||
} else {
|
||||
log::warn!("scaletempo element not available; speed changes will affect pitch");
|
||||
}
|
||||
pipeline.connect("element-setup", false, |vals| {
|
||||
let Ok(elem) = vals[1].get::<gst::Element>() else {
|
||||
return None;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue