Compare commits

..

23 commits

Author SHA1 Message Date
4a1bfc975c fix: wgpu 28 port + background accessor post-rebase
Some checks failed
Validate .desktop files / validate (push) Has been cancelled
Leyoda 2026 – GPLv3
2026-07-04 09:52:35 +02:00
d437199eb5 feat: add about drawer 2026-07-04 09:46:43 +02:00
bda5578c69 chore: use local cosmic-text checkout 2026-07-04 09:45:20 +02:00
324655a955 player: register MPEG and WMV support cleanly 2026-07-04 09:45:20 +02:00
761c0d4ae1 iced_video_player: silence local check warnings 2026-07-04 09:45:20 +02:00
419bc79e07 local: add cosmic-player redeploy helper 2026-07-04 09:45:20 +02:00
a8497eaf21 local: integrate yoda libcosmic and fix thumbnail hang 2026-07-04 09:45:20 +02:00
Jeremy Soller
b162e335db Epoch 1.2.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-06-30 13:24:24 -06:00
Jeremy Soller
f6d3554788 Epoch 1.1.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-06-23 10:39:31 -06:00
Jeremy Soller
c0b1bda86b Epoch 1.0.16 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-06-09 12:23:41 -06:00
jklgrasso
26465d8d1b Fix frameskip playhead handling 2026-06-05 18:32:28 -04:00
Jeremy Soller
2c1ace7170
Merge pull request #283 from xDAndrew/bug/forward-seek-button-fix
Fix seek direction handling and clamp forward seeking
2026-06-03 11:54:24 -06:00
Andrei Ivanou
a853ed23a5 Clamp forward seek and sync frame stepping position 2026-06-03 17:46:11 +02:00
Andrei Ivanou
26f396e4c8 Fix playback speed configuration after change playback options 2026-06-03 10:44:40 -04:00
Andrei Ivanou
8ad4f00c26 Add playback speed controls & minor UI \ UX improvements 2026-06-03 10:44:40 -04:00
Jeremy Soller
ddf43efc48 Epoch 1.0.15 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-06-02 11:58:44 -06:00
Jeremy Soller
31ca4f7af6
Merge pull request #272 from jklgrasso/testing-doc
Add TESTING.md doc
2026-05-26 15:47:51 -06:00
Jeremy Soller
be02779570 Fixes for new libcosmic 2026-05-26 13:14:56 -06:00
Levi Portenier
2817e7e778
Merge pull request #276 from pop-os/epoch-update
Epoch 1.0.14 version update
2026-05-26 11:36:25 -06:00
Jeremy Soller
7becbe6f71
Merge pull request #275 from weblate/weblate-pop-os-cosmic-player
i18n: translation update from Hosted Weblate
2026-05-26 09:12:00 -06:00
Jeremy Soller
67eda26107 Epoch 1.0.14 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-05-26 08:31:21 -06:00
Hosted Weblate
1c5fcc2e98
i18n: translation updates from weblate
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-player/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-player/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-player/zh_Hant/
Translation: Pop OS/COSMIC Player
2026-05-25 22:11:34 +00:00
jklgrasso
c46822de6b Add TESTING.md doc 2026-05-06 12:17:17 -06:00
7 changed files with 522 additions and 480 deletions

1
.gitignore vendored
View file

@ -10,3 +10,4 @@
!/vendor/iced_video_player/
!/vendor/iced_video_player/**
/vendor.tar
.idea/

748
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-player"
version = "1.0.14"
version = "1.2.0"
edition = "2024"
rust-version = "1.93"

24
debian/changelog vendored
View file

@ -1,3 +1,27 @@
cosmic-player (1.2.0) noble; urgency=medium
* Epoch 1.2.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 30 Jun 2026 12:21:32 -0600
cosmic-player (1.1.0) noble; urgency=medium
* Epoch 1.1.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 23 Jun 2026 08:48:43 -0600
cosmic-player (1.0.16) noble; urgency=medium
* Epoch 1.0.16 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 09 Jun 2026 10:26:19 -0600
cosmic-player (1.0.15) noble; urgency=medium
* Epoch 1.0.15 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 02 Jun 2026 11:02:44 -0600
cosmic-player (1.0.14) noble; urgency=medium
* Epoch 1.0.14 version update

View 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

View file

@ -55,6 +55,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> {
@ -229,6 +232,7 @@ pub struct Flags {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DropdownKind {
Audio,
Speed,
Subtitle,
}
@ -293,7 +297,7 @@ pub enum Message {
MultipleLoad(Vec<url::Url>),
Fullscreen,
LaunchUrl(String),
Key(Modifiers, Key),
Key(Modifiers, Physical, Key),
AudioCode(usize),
AudioToggle,
AudioVolume(f64),
@ -316,6 +320,8 @@ pub enum Message {
NewFrame,
Reload,
AbRepeat,
VideoAreaClick,
PlaybackSpeed(f64),
ShowControls,
SystemThemeModeChange(cosmic_theme::ThemeMode),
ToggleAbout,
@ -348,6 +354,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>,
}
@ -462,6 +469,11 @@ impl App {
self.set_looping_from_repeat_state();
self.update_flags();
self.update_mpris_meta();
if self.playback_speed != 1.0 {
if let Some(video) = &mut self.video_opt {
Self::apply_speed(video, self.playback_speed);
}
}
self.update_title()
}
@ -854,6 +866,12 @@ impl App {
video.set_looping(self.flags.config_state.player_state.repeat == RepeatState::Track);
}
}
fn apply_speed(video: &mut Video, speed: f64) {
if let Err(err) = video.set_speed(speed) {
log::warn!("failed to set playback speed {}: {}", speed, err);
}
}
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
@ -931,6 +949,7 @@ impl Application for App {
text_codes: Vec::new(),
current_text: None,
ab_repeat: None,
playback_speed: 1.0,
#[cfg(feature = "xdg-portal")]
inhibit,
};
@ -1232,9 +1251,9 @@ impl Application for App {
log::warn!("failed to open URL {:?}: {}", url, err);
}
},
Message::Key(modifiers, key) => {
Message::Key(modifiers, physical, key) => {
for (key_bind, action) in self.key_binds.iter() {
if key_bind.matches(modifiers, &key, None) {
if key_bind.matches(modifiers, &key, Some(&physical)) {
return self.update(action.message());
}
}
@ -1366,10 +1385,10 @@ impl Application for App {
}
Message::SeekRelative(secs) => {
if let Some(video) = &mut self.video_opt {
self.position = video.position().as_secs_f64();
let duration =
Duration::try_from_secs_f64(self.position + secs).unwrap_or_default();
video.seek(duration, true).expect("seek");
self.position =
(video.position().as_secs_f64() + secs).clamp(0.0, self.duration);
let target = Duration::try_from_secs_f64(self.position).unwrap_or_default();
video.seek(target, true).expect("seek");
}
}
Message::SeekRelease => {
@ -1491,13 +1510,19 @@ impl Application for App {
if let Some(video) = &mut self.video_opt
&& video.has_video()
{
video.set_paused(true);
video.pipeline().send_event(gst::event::Step::new(
gst::format::Buffers::from_u64(1),
1.0,
true,
false,
));
video.set_paused(true);
let fps = get_framerate(video).unwrap_or(30.0);
let frame_duration = Duration::from_secs_f64(1.0 / fps);
self.position = (video.position().as_secs_f64() + frame_duration.as_secs_f64())
.clamp(0.0, self.duration);
self.update_controls(true);
}
}
@ -1506,16 +1531,33 @@ impl Application for App {
&& video.has_video()
{
// TODO: Improve Accuracy.
video.set_paused(true);
let current = video.position();
let fps = get_framerate(video).unwrap_or(30.0);
let frame_duration = Duration::from_secs_f64(1.0 / fps);
let target = current.saturating_sub(frame_duration + Duration::from_millis(1));
self.position = target.as_secs_f64().clamp(0.0, self.duration);
video.seek(target, true).expect("seek");
video.set_paused(true);
self.update_controls(true);
}
}
Message::PlaybackSpeed(speed) => {
self.playback_speed = speed;
self.dropdown_opt = None;
if let Some(video) = &mut self.video_opt {
Self::apply_speed(video, 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 {
@ -1793,7 +1835,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);
@ -1830,6 +1872,58 @@ 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());
@ -1858,34 +1952,42 @@ 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::container(column)
.padding(1)
//TODO: move style to libcosmic
.class(theme::Container::custom(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(false).component;
widget::container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),
border: Border {
radius: 8.0.into(),
width: 1.0,
color: component.divider.into(),
},
..Default::default()
}
}))
.width(Length::Fixed(240.0))
.into(),
widget::mouse_area(
widget::container(column)
.padding(1)
//TODO: move style to libcosmic
.class(theme::Container::custom(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(false).component;
widget::container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),
border: Border {
radius: 8.0.into(),
width: 1.0,
color: component.divider.into(),
},
..Default::default()
}
}))
.width(Length::Fixed(240.0)),
)
.on_press(Message::ShowControls)
.into(),
])
.into(),
);
@ -1981,6 +2083,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),
@ -2008,31 +2116,37 @@ impl Application for App {
.on_press(Message::DropdownToggle(DropdownKind::Audio)),
);
popup_items.push(
widget::container(row)
.padding([space_xxs, space_xs])
.class(theme::Container::WindowBackground)
.into(),
widget::mouse_area(
widget::container(row)
.padding([space_xxs, space_xs])
.class(theme::Container::WindowBackground),
)
.on_press(Message::ShowControls)
.into(),
);
if self.core.is_condensed() {
popup_items.push(
widget::container(
widget::row::with_capacity(3)
.align_y(Alignment::Center)
.spacing(space_xxs)
.push(widget::text(format_time(self.position)).font(font::mono()))
.push(
Slider::new(0.0..=self.duration, self.position, Message::Seek)
.step(0.1)
.on_release(Message::SeekRelease),
)
.push(
widget::text(format_time(self.duration - self.position))
.font(font::mono()),
),
widget::mouse_area(
widget::container(
widget::row::with_capacity(3)
.align_y(Alignment::Center)
.spacing(space_xxs)
.push(widget::text(format_time(self.position)).font(font::mono()))
.push(
Slider::new(0.0..=self.duration, self.position, Message::Seek)
.step(0.1)
.on_release(Message::SeekRelease),
)
.push(
widget::text(format_time(self.duration - self.position))
.font(font::mono()),
),
)
.padding([space_xxs, space_xs])
.class(theme::Container::WindowBackground),
)
.padding([space_xxs, space_xs])
.class(theme::Container::WindowBackground)
.on_press(Message::ShowControls)
.into(),
);
}
@ -2067,7 +2181,7 @@ impl Application for App {
physical_key,
key,
..
}) => Some(Message::Key(modifiers, key)),
}) => Some(Message::Key(modifiers, physical_key, key)),
Event::Mouse(MouseEvent::CursorMoved { .. }) => Some(Message::ShowControls),
Event::Mouse(MouseEvent::WheelScrolled { delta }) => Some(Message::Scrolled(delta)),
_ => None,

View file

@ -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;