Merge branch 'master' into feat/repeat-toggle

This commit is contained in:
Will Sheehan 2026-02-20 23:15:50 -08:00
commit f161863c09
37 changed files with 1639 additions and 950 deletions

8
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,8 @@
- [ ] I have disclosed use of any AI generated code in my commit messages.
- If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment.
- [ ] I understand these changes in full and will be able to respond to review comments.
- [ ] My change is accurately described in the commit message.
- [ ] My contribution is tested and working as described.
- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.

2274
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-player"
version = "0.1.0"
version = "1.0.7"
edition = "2024"
[build-dependencies]
@ -56,6 +56,11 @@ wgpu = ["iced_video_player/wgpu", "libcosmic/wgpu"]
inherits = "release"
debug = true
# Keep cosmic-text at version compatible with prev-master of libcosmic
[patch.'https://github.com/pop-os/cosmic-text'.cosmic-text]
git = "https://github.com/pop-os/cosmic-text//"
rev = "166b59f560c551dab391a864f7c1f503c1e18446"
# [patch.'https://github.com/jackpot51/iced_video_player']
# iced_video_player = { path = "../../iced_video_player" }

24
debian/changelog vendored
View file

@ -1,3 +1,27 @@
cosmic-player (1.0.7) noble; urgency=medium
* Epoch 1.0.7 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 17 Feb 2026 07:58:49 -0700
cosmic-player (1.0.6) noble; urgency=medium
* Epoch 1.0.6 version update
-- Jeremy Soller <jeremy@system76.com> Thu, 05 Feb 2026 15:23:17 -0700
cosmic-player (1.0.5) noble; urgency=medium
* Epoch 1.0.5 version update
-- Jeremy Soller <jeremy@system76.com> Fri, 30 Jan 2026 17:16:34 -0700
cosmic-player (1.0.4) noble; urgency=medium
* Epoch 1.0.4 version update
-- Jeremy Soller <jeremy@system76.com> Wed, 21 Jan 2026 10:16:51 -0700
cosmic-player (0.1.0) jammy; urgency=medium
* Initial release.

View file

@ -1,7 +1,7 @@
album = ألبوم: { $album }
audio = الصوت
no-video-or-audio-file-open = لا يوجد ملف فيديو أو صوت مفتوح
open-file = افتح ملف
open-file = افتح ملفًا
open-folder = افتح مجلد
subtitles = الترجمة
unknown-author = مؤلف مجهول
@ -17,7 +17,7 @@ settings = الإعدادات
### Appearance
appearance = المظهر
theme = الثيم
theme = النسق
match-desktop = طابق سطح المكتب
dark = داكن
light = فاتح
@ -28,11 +28,12 @@ light = فاتح
## File
file = ملف
open-media = فتح الوسائط...
open-recent-media = فتح الوسائط الحديثة
close-file = إغلاق الملف
open-media = افتح الوسائط...
open-recent-media = افتح الوسائط الحديثة
close-file = أغلِق الملف
open-media-folder = فتح مجلد الوسائط...
open-recent-media-folder = فتح مجلد الوسائط الحديثة
close-media-folder = غلق مجلد الوسائط
close-media-folder = أغلِق مجلد الوسائط
quit = أنهِ
off = معطل
off = معطّل
clear-recent = امحُ القائمة الحديثة

View file

@ -21,3 +21,4 @@ close-media-folder = Закрыць медыя папку
quit = Выйсці
off = Выкл.
match-desktop = Як у сістэме
clear-recent = Ачысціць спіс нядаўніх

View file

@ -8,10 +8,10 @@ unknown-author = Neznámý autor
untitled = Bez názvu
settings = Nastavení
appearance = Vzhled
theme = Téma
theme = Motiv
match-desktop = Podle systému
dark = Tmavé
light = Světlé
dark = Tmavý
light = Světlý
file = Soubor
open-media = Otevřít média...
open-recent-media = Otevřít nedávná média

View file

@ -18,7 +18,7 @@ settings = Socruithe
appearance = Cuma
theme = Téama
match-desktop = Meaitseáil leis an deasc
match-desktop = Meaitseáil deasc
dark = Dorcha
light = Solas
@ -28,7 +28,7 @@ light = Solas
## File
file = Comhad
open-media = Meáin oscailte...
open-media = Oscail meáin...
open-recent-media = Oscail meáin le déanaí
close-file = Dún an comhad
open-media-folder = Oscail fillteán meán...
@ -36,3 +36,4 @@ open-recent-media-folder = Oscail fillteán meán le déanaí
close-media-folder = Dún an fillteán meán
quit = Scoir
off = As
clear-recent = Glan an liosta le déanaí

View file

@ -28,10 +28,10 @@ light = Világos
## File
file = Fájl
open-media = Médiafájl megnyitása...
open-media = Médiafájl megnyitása
open-recent-media = Legutóbbi médiafájl megnyitása
close-file = Fájl bezárása
open-media-folder = Médiamappa megnyitása...
open-media-folder = Médiamappa megnyitása
open-recent-media-folder = Legutóbbi médiamappa megnyitása
close-media-folder = Médiamappa bezárása
quit = Kilépés

View file

@ -0,0 +1,24 @@
album = Album: { $album }
open-file = Buka berkas
audio = Audio
no-video-or-audio-file-open = Tidak ada berkas video atau audio yang dibuka
quit = Keluar
off = Mati
open-folder = Buka map
subtitles = Subjudul
unknown-author = Penulis Tidak Diketahui
settings = Pengaturan
untitled = Tidak Berjudul
appearance = Tampilan
theme = Tema
match-desktop = Cocokkan desktop
dark = Gelap
light = Terang
file = Berkas
close-file = Tutup berkas
open-media = Buka media...
open-recent-media = Buka media terbaru
open-media-folder = Buka map media...
open-recent-media-folder = Buka map media terbaru
close-media-folder = Tutup map media
clear-recent = Hapus daftar terbaru

View file

View file

24
i18n/kk/cosmic_player.ftl Normal file
View file

@ -0,0 +1,24 @@
open-file = Файлды ашу
open-folder = Буманы ашу
settings = Баптаулар
appearance = Сыртқы түрі
theme = Тақырып
match-desktop = Жұмыс үстеліне сәйкес келу
dark = Қараңғы
light = Жарық
file = Файл
quit = Шығу
close-file = Файлды жабу
album = Альбом: { $album }
audio = Аудио
no-video-or-audio-file-open = Ашық видео немесе аудио файл жоқ
off = Сөндірулі
subtitles = Субтитрлер
unknown-author = Белгісіз автор
untitled = Атаусыз
open-media = Медианы ашу...
open-recent-media = Жуырдағы медианы ашу
clear-recent = Жуырдағылар тізімін тазалау
open-media-folder = Медиа бумасын ашу...
open-recent-media-folder = Жуырдағы медиа бумасын ашу
close-media-folder = Медиа бумасын жабу

View file

View file

@ -1,11 +1,15 @@
# Context Pages
## Settings
settings = ಸೆಟ್ಟಿಂಗ್ಸ್
### Appearance
appearance = ರೂಪ
theme = ಥೀಮ್
match-desktop = ಡೆಸ್ಕ್‌ಟಾಪ್‌ಗೆ ಹೊಂದಿಸಿ
dark = ಡಾರ್ಕ್
light = ಲೈಟ್
album = ಆಲ್ಬಮ್: { $album }

View file

@ -0,0 +1,24 @@
settings = 설정
appearance = 외관
theme = 테마
file = 파일
quit = 종료
open-file = 파일 열기
open-folder = 폴더 열기
audio = 오디오
subtitles = 자막
untitled = 무제
dark = 다크
light = 라이트
match-desktop = 시스템과 동기화
unknown-author = 알 수 없는 저자
open-media = 미디어 열기...
open-recent-media = 최근 미디어 열기
clear-recent = 최근 목록 제거하기
open-media-folder = 미디어 폴더 열기...
open-recent-media-folder = 최근 미디어 폴더 열기
close-media-folder = 미디어 폴더 닫기
no-video-or-audio-file-open = 비디오 혹은 오디오가 열려있지 않음
off = 끄기
album = 앨범: { $album }
close-file = 파일 닫기

View file

@ -0,0 +1,22 @@
quit = Išeiti
settings = Nustatymai
close-file = Uždaryti failą
match-desktop = Pagal darbalaukio temą
appearance = Išvaizda
theme = Stilius
file = Failas
album = Albumas: { $album }
audio = Garsas
open-file = Atidaryti failą
no-video-or-audio-file-open = Neatidarytas vaizdo ar garso failas
off = Išjungtas
open-folder = Atidaryti aplanką
subtitles = Subtitrai
unknown-author = Autorius nežinomas
untitled = Be pavadinimo
open-media = Atidaryti audiovizualą...
open-recent-media = Atidaryti neseniai peržiūrėtą audiovizualą
clear-recent = Išvalyti neseniai peržiūrėtų sąrašą
open-media-folder = Atidaryti audiovizualų aplanką...
open-recent-media-folder = Atidaryti neseniai peržiūrėtą audiovizualų aplanką
close-media-folder = Užverti audiovizualų aplanką

View file

View file

View file

@ -1,8 +1,8 @@
album = Album: { $album }
audio = Audio
no-video-or-audio-file-open = Geen video- of audiobestand geopend
open-file = Open bestand
open-folder = Open een map
open-file = Bestand openen
open-folder = Map openen
subtitles = Ondertiteling
unknown-author = Onbekende auteur
untitled = Geen titel
@ -18,7 +18,7 @@ settings = Instellingen
appearance = Uiterlijk
theme = Thema
match-desktop = Maak gelijk aan bureaublad
match-desktop = Systeemstandaard
dark = Donker
light = Licht
@ -28,11 +28,11 @@ light = Licht
## File
file = Bestand
open-media = Media openen...
open-recent-media = Open recente media
close-file = Sluit bestand
open-media-folder = Open mediamap
open-recent-media-folder = Open recente mediamap
open-media = Media openen
open-recent-media = Recente media openen
close-file = Bestand sluiten
open-media-folder = Mediamap openen
open-recent-media-folder = Recente mediamap openen
close-media-folder = Sluit mediamap
quit = Beëindig
quit = Sluiten
clear-recent = Wis recente lijst

View file

View file

View file

@ -36,3 +36,4 @@ open-recent-media-folder = Abrir pasta recente de mídias
close-media-folder = Fechar pasta de mídias
quit = Sair
off = Desligado
clear-recent = Limpar a lista recente

View file

@ -10,8 +10,8 @@ settings = Параметры
appearance = Оформление
theme = Тема
match-desktop = Как в системе
dark = Тёмное
light = Светлое
dark = Тёмная
light = Светлая
open-file = Открыть файл
album = Альбом: { $album }
audio = Аудио
@ -22,10 +22,11 @@ unknown-author = Неизвестный автор
untitled = Без названия
file = Файл
open-media = Открыть медиафайл…
open-recent-media = Недавние медиафайлы
open-recent-media = Открыть недавний медиафайл
close-file = Закрыть файл
open-media-folder = Открыть папку медиафайлов…
open-recent-media-folder = Недавние папки медиафайлов
close-media-folder = Закрыть папку медиафайлов
quit = Выйти
off = Выкл.
clear-recent = Очистить список недавних

View file

View file

@ -9,16 +9,16 @@ untitled = Без назви
settings = Налаштування
appearance = Зовнішній вигляд
theme = Тема
match-desktop = Відповідно системі
match-desktop = Системна
dark = Темна
light = Світла
file = Файл
open-media = Відкрити медіа...
open-recent-media = Відкрити нещодавнє медіа
open-recent-media = Відкрити останні медіа
close-file = Закрити файл
open-media-folder = Відкрити теку з медіа...
open-recent-media-folder = Відкрити нещодавню теку з медіа
close-media-folder = Закрити теку з медіа
quit = Вийти
off = Вимк.
clear-recent = Очистити нещодавній список
clear-recent = Очистити список

View file

View file

View file

@ -1,4 +1,4 @@
album = 专辑: { $album }
album = 专辑{ $album }
audio = 音频
no-video-or-audio-file-open = 未打开视频或音频文件
open-file = 打开文件
@ -29,9 +29,11 @@ light = 亮色模式
file = 文件
open-media = 打开媒体文件...
open-recent-media = 打开最近媒体文件
open-recent-media = 打开最近媒体文件
close-file = 关闭文件
open-media-folder = 打开媒体文件夹...
open-recent-media-folder = 打开最近媒体文件夹
open-recent-media-folder = 打开最近媒体文件夹
close-media-folder = 关闭媒体文件夹
quit = 退出
off = 已关闭
clear-recent = 清空近期列表

View file

@ -8,9 +8,11 @@ Name[hu]=COSMIC Médialejátszó
Name[pl]=Odtwarzacz Multimediów COSMIC
Name[pt_BR]=Reprodutor de Mídia
Name[pt]=Reprodutor de Mídia
Name[ru]=Медиаплеер
Name[sk]=Prehrávač Médií COSMIC
Name[sv]=COSMIC Mediaspelare
Name[es]=Reproductor de medios COSMIC
Name[it]=COSMIC Media Player
Exec=cosmic-player %U
Terminal=false
Type=Application
@ -24,7 +26,9 @@ Keywords[cs]=audio;film;hudba;video;média;zvuk;
Keywords[hu]=hang;film;zene;videó;média;lejátszó;
Keywords[pl]=Audio;Dźwięki;Filmy;Muzyka;Vídeo;Media;Odtwarzacz;Multimedia;
Keywords[pt_BR]=Audio;Filme;Música;Som;Vídeo;Media;Player;
Keywords[ru]=Аудио;Фильм;Музыка;Видео;Медиа;Звук;Мультимедиа;Медиаплеер;
Keywords[pt]=Audio;Filme;Música;Som;Vídeo;Media;Player;
Keywords[sk]=audio;film;hudba;video;prehrávač;média;zvuk;
Keywords[sv]=Ljud;Film;Musik;Video;Media;
Keywords[es]=Audio;Película;Música;Vídeo;Sonido;Reproductor;
Keywords[it]=Audio;Video;Musica;Film;Suoni;Riproduzione;

View file

@ -17,6 +17,7 @@
<name xml:lang="pt">Reprodutor de Mídia</name>
<name xml:lang="sk">Prehrávač Médií COSMIC</name>
<name xml:lang="es">Reproductor de medios COSMIC</name>
<name xml:lang="it">COSMIC Media Player</name>
<name xml:lang="sv">COSMIC Mediaspelare</name>
<summary>Media player for the COSMIC desktop</summary>
<summary xml:lang="ar">مشغّل وسائط لسطح مكتب COSMIC</summary>
@ -27,6 +28,7 @@
<summary xml:lang="pt">Reprodutor de mídia do desktop COSMIC</summary>
<summary xml:lang="sk">Prehrávač médií pre pracovné prostredie COSMIC</summary>
<summary xml:lang="es">Reproductor de medios para el escritorio COSMIC</summary>
<summary xml:lang="it">Lettore multimediale di COSMIC</summary>
<summary xml:lang="sv">Mediaspelare för skrivbordsmiljön COSMIC</summary>
<description>
<p>Media player for the COSMIC desktop</p>
@ -37,6 +39,7 @@
<p xml:lang="pt_BR">Reprodutor de mídia do desktop COSMIC</p>
<p xml:lang="pt">Reprodutor de mídia do desktop COSMIC</p>
<p xml:lang="sk">Prehrávač médií pre pracovné prostredie COSMIC</p>
<p xml:lang="it">Lettore multimediale di COSMIC</p>
<p xml:lang="es">Reproductor de medios para el escritorio COSMIC</p>
<p xml:lang="sv">Mediaspelare för skrivbordsmiljön COSMIC</p>
</description>

View file

@ -81,7 +81,6 @@ pub fn parse() -> Arguments {
if urls.len() > 1 {
arguments.urls = Some(urls);
} else {
urls.truncate(1);
arguments.url_opt = urls.pop();
}

View file

@ -2,25 +2,23 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
Application, ApplicationExt, Element,
app::{Command, Core, Settings, command, message},
app::{command, message, Command, Core, Settings},
cosmic_config::{self, CosmicConfigEntry},
cosmic_theme, executor, font,
iced::{
Alignment, Background, Border, Color, ContentFit, Length, Limits,
event::{self, Event},
keyboard::{Event as KeyEvent, Key, Modifiers},
mouse::{Event as MouseEvent, ScrollDelta},
subscription::Subscription,
window,
window, Alignment, Background, Border, Color, ContentFit, Length, Limits,
},
iced_style, theme,
widget::{self, Slider, menu::action::MenuAction, nav_bar, segmented_button},
widget::{self, menu::action::MenuAction, nav_bar, segmented_button, Slider},
Application, ApplicationExt, Element,
};
use iced_video_player::{
Video, VideoPlayer,
gst::{self, prelude::*},
gst_pbutils,
gst_pbutils, Video, VideoPlayer,
};
use std::{
any::TypeId,
@ -34,8 +32,8 @@ use std::{
use tokio::sync::mpsc;
use crate::{
config::{CONFIG_VERSION, Config, ConfigState, RepeatState},
key_bind::{KeyBind, key_binds},
config::{Config, ConfigState, CONFIG_VERSION, RepeatState},
key_bind::{key_binds, KeyBind},
project::ProjectNode,
};
@ -286,6 +284,7 @@ pub enum Message {
Seek(f64),
SeekRelative(f64),
SeekRelease,
PlayNext,
EndOfStream,
MissingPlugin(gst::Message),
MprisChannel(MprisMeta, MprisState, mpsc::UnboundedSender<MprisEvent>),
@ -371,7 +370,7 @@ impl App {
self.flags.config_state.recent_files.truncate(10);
self.save_config_state();
let video = match video::new_video(&url) {
let video = match video::new_video(&url, video::VideoSettings::default()) {
Ok(ok) => ok,
Err(err) => return err,
};
@ -908,7 +907,8 @@ impl Application for App {
let command = match (app.flags.urls.take(), maybe_path) {
(Some(urls), _) => command::message::app(Message::MultipleLoad(urls)),
(None, Some(path)) if path.is_dir() => command::message::app(Message::FolderLoad(path)),
_ => app.load(),
_ => app.load(), //If there is no url args, we execute load for nothing?
//If only one file is loaded, nothing is added to the navbar.
};
(app, command)
}
@ -929,9 +929,7 @@ impl Application for App {
// Toggle open state and get clone of node data
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
Some(node) => {
if let ProjectNode::Folder { open, .. } = node {
*open = !*open;
}
node.flip_open();
Some(node.clone())
}
None => None,
@ -1196,6 +1194,7 @@ impl Application for App {
if let Some(video) = &mut self.video_opt {
if volume >= 0.0 && volume <= 1.0 {
video.set_volume(volume);
video.set_muted(false);
self.update_controls(true);
}
}
@ -1282,6 +1281,7 @@ impl Application for App {
if (volume >= 0.0 && volume <= 1.0) && !nav_bar_toggled {
video.set_volume(volume);
video.set_muted(false);
self.update_controls(true);
}
}
@ -1320,12 +1320,61 @@ impl Application for App {
self.update_controls(true);
}
}
Message::PlayNext => {
// TODO: known limitations:
// 1) if the user collapses the folder entry while a song is playing,
// the player will stop at the end of the stream because the current ID may become `Entity(null)`.
//
// 2) ProjectNode::File does not restrict file types to those supported by GStreamer.
// Therefore, if a non-playable file (e.g., a .jpg) is encountered in a folder, it will trigger a
// "failed to open file" error and halt the stream.
//
// 3) if we play the last song of a folder and the next one is already expanded by
// user (or because it was played before), the player will collapse it and jump
// to the next file/folder after it.
//first we get info about current media id & position in nav_bar
let curr_id = self.nav_model.active();
let curr_position = match self.nav_model.position(curr_id) {
Some(pos) => pos,
None => {
log::warn!("Failed to get position of current media: {:?}", curr_id);
return self.update(Message::EndOfStream);
}
};
//Then we activate the next one in the nav bar and ask to load it
if self.nav_model.activate_position(curr_position + 1) {
let curr_id = self.nav_model.active();
match self.nav_model.data::<ProjectNode>(curr_id) {
//The next one is a media file, we play it.
Some(ProjectNode::File { .. }) => return self.on_nav_select(curr_id),
//The next one is a folder. We expand it and recall PlayNext.
Some(ProjectNode::Folder { .. }) => {
let _ = self.on_nav_select(curr_id);
return self.update(Message::PlayNext);
}
//Unknown type. We do nothing.
_ => log::warn!(
"unknown type: {:?}",
self.nav_model.data::<ProjectNode>(curr_id)
),
}
} else {
return self.update(Message::EndOfStream);
}
}
Message::EndOfStream => {
println!(
"end of stream, repeat={:?}",
self.flags.config_state.player_state.repeat
);
}
Message::MissingPlugin(element) => {
if let Some(video) = &mut self.video_opt {
video.set_paused(true);
@ -1474,7 +1523,7 @@ impl Application for App {
let mut video_player: Element<_> = VideoPlayer::new(video)
.mouse_hidden(!self.controls)
.on_duration_changed(Message::DurationChanged)
.on_end_of_stream(Message::EndOfStream)
.on_end_of_stream(Message::PlayNext)
.on_missing_plugin(Message::MissingPlugin)
.on_new_frame(Message::NewFrame)
.width(Length::Fill)
@ -1570,7 +1619,6 @@ impl Application for App {
)
.on_press(Message::AudioToggle)
.into(),
//TODO: disable slider when muted?
Slider::new(0.0..=1.0, volume, Message::AudioVolume)
.step(0.01)
.into(),

View file

@ -70,6 +70,12 @@ impl ProjectNode {
Self::File { name, .. } => name,
}
}
pub fn flip_open(&mut self) {
if let Self::Folder { open, .. } = self {
*open = !*open;
}
}
}
impl Ord for ProjectNode {

View file

@ -13,7 +13,7 @@ pub fn main(
) -> Result<(), Box<dyn Error>> {
let mut image = {
let thumbnails = {
let mut video = match video::new_video(input) {
let mut video = match video::new_video(input, video::VideoSettings { mute: true }) {
Ok(ok) => ok,
Err(_err) => return Err(Into::into(format!("missing required plugin"))),
};

View file

@ -6,16 +6,23 @@ use iced_video_player::{
use cosmic::app::{Command, message};
#[derive(Debug, Default)]
pub struct VideoSettings {
pub mute: bool,
}
pub fn new_video(
url: &url::Url,
settings: VideoSettings,
) -> Result<Video, cosmic::Command<cosmic::app::Message<super::Message>>> {
//TODO: this code came from iced_video_player::Video::new and has been modified to stop the pipeline on error
//TODO: remove unwraps and enable playback of files with only audio.
gst::init().unwrap();
let pipeline = format!(
"playbin uri=\"{}\" video-sink=\"videoscale ! videoconvert ! videoflip method=automatic ! appsink name=iced_video drop=true caps=video/x-raw,format=NV12,pixel-aspect-ratio=1/1\"",
url.as_str()
"playbin uri=\"{}\"{} video-sink=\"videoscale ! videoconvert ! videoflip method=automatic ! appsink name=iced_video drop=true caps=video/x-raw,format=NV12,pixel-aspect-ratio=1/1\"",
url.as_str(),
if settings.mute { " mute=true" } else { "" }
);
let pipeline = gst::parse::launch(pipeline.as_ref())
.unwrap()