commit
069337e2ca
8 changed files with 2007 additions and 1688 deletions
3428
Cargo.lock
generated
3428
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
19
Cargo.toml
19
Cargo.toml
|
|
@ -30,15 +30,14 @@ env_logger = "0.10"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
||||||
[dependencies.iced_video_player]
|
[dependencies.iced_video_player]
|
||||||
git = "https://github.com/jackpot51/iced_video_player.git"
|
git = "https://github.com/wash2/iced_video_player.git"
|
||||||
branch = "prev-cosmic"
|
branch = "iced-rebase"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[dependencies.libcosmic]
|
[dependencies.libcosmic]
|
||||||
git = "https://github.com/pop-os/libcosmic.git"
|
git = "https://github.com/pop-os/libcosmic.git"
|
||||||
branch = "prev-master"
|
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["tokio", "winit"]
|
features = ["tokio", "winit", "multi-window"]
|
||||||
|
|
||||||
[dependencies.mpris-server]
|
[dependencies.mpris-server]
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
|
@ -56,15 +55,13 @@ wgpu = ["iced_video_player/wgpu", "libcosmic/wgpu"]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
# Keep cosmic-text at version compatible with prev-master of libcosmic
|
# [patch.'https://github.com/wash2/iced_video_player']
|
||||||
[patch.'https://github.com/pop-os/cosmic-text'.cosmic-text]
|
# iced_video_player = { path = "../iced_video_player" }
|
||||||
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" }
|
|
||||||
|
|
||||||
# [patch.'https://github.com/pop-os/libcosmic']
|
# [patch.'https://github.com/pop-os/libcosmic']
|
||||||
# libcosmic = { path = "../libcosmic" }
|
# libcosmic = { path = "../libcosmic" }
|
||||||
# cosmic-config = { path = "../libcosmic/cosmic-config" }
|
# cosmic-config = { path = "../libcosmic/cosmic-config" }
|
||||||
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
|
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
|
||||||
|
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||||
|
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||||
|
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ pub fn key_binds() -> HashMap<KeyBind, Action> {
|
||||||
//TODO: key bindings
|
//TODO: key bindings
|
||||||
bind!([], Key::Character("f".into()), Fullscreen);
|
bind!([], Key::Character("f".into()), Fullscreen);
|
||||||
bind!([Alt], Key::Named(Named::Enter), Fullscreen);
|
bind!([Alt], Key::Named(Named::Enter), Fullscreen);
|
||||||
bind!([], Key::Named(Named::Space), PlayPause);
|
bind!([], Key::Character(" ".into()), PlayPause);
|
||||||
bind!([], Key::Named(Named::ArrowLeft), SeekBackward);
|
bind!([], Key::Named(Named::ArrowLeft), SeekBackward);
|
||||||
bind!([], Key::Named(Named::ArrowRight), SeekForward);
|
bind!([], Key::Named(Named::ArrowRight), SeekForward);
|
||||||
|
|
||||||
|
|
|
||||||
176
src/main.rs
176
src/main.rs
|
|
@ -2,17 +2,19 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
app::{command, message, Command, Core, Settings},
|
action::{self, app},
|
||||||
|
app::{Core, Settings, Task},
|
||||||
|
command::set_theme,
|
||||||
cosmic_config::{self, CosmicConfigEntry},
|
cosmic_config::{self, CosmicConfigEntry},
|
||||||
cosmic_theme, executor, font,
|
cosmic_theme, executor, font,
|
||||||
iced::{
|
iced::{
|
||||||
event::{self, Event},
|
event::{self, Event},
|
||||||
keyboard::{Event as KeyEvent, Key, Modifiers},
|
keyboard::{Event as KeyEvent, Key, Modifiers},
|
||||||
mouse::{Event as MouseEvent, ScrollDelta},
|
mouse::{Event as MouseEvent, ScrollDelta},
|
||||||
subscription::Subscription,
|
window::{self, set_mode},
|
||||||
window, Alignment, Background, Border, Color, ContentFit, Length, Limits,
|
Alignment, Background, Border, Color, ContentFit, Length, Limits, Subscription,
|
||||||
},
|
},
|
||||||
iced_style, theme,
|
theme,
|
||||||
widget::{self, menu::action::MenuAction, nav_bar, segmented_button, Slider},
|
widget::{self, menu::action::MenuAction, nav_bar, segmented_button, Slider},
|
||||||
Application, ApplicationExt, Element,
|
Application, ApplicationExt, Element,
|
||||||
};
|
};
|
||||||
|
|
@ -26,7 +28,12 @@ use std::{
|
||||||
ffi::{CStr, CString},
|
ffi::{CStr, CString},
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process, thread,
|
process,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
thread,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
@ -34,6 +41,7 @@ use tokio::sync::mpsc;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, ConfigState, CONFIG_VERSION, RepeatState},
|
config::{Config, ConfigState, CONFIG_VERSION, RepeatState},
|
||||||
key_bind::{key_binds, KeyBind},
|
key_bind::{key_binds, KeyBind},
|
||||||
|
mpris::subscription,
|
||||||
project::ProjectNode,
|
project::ProjectNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -254,7 +262,6 @@ impl AsRef<str> for TextCode {
|
||||||
/// Messages that are used specifically by our [`App`].
|
/// Messages that are used specifically by our [`App`].
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
None,
|
|
||||||
Config(Config),
|
Config(Config),
|
||||||
ConfigState(ConfigState),
|
ConfigState(ConfigState),
|
||||||
DropdownToggle(DropdownKind),
|
DropdownToggle(DropdownKind),
|
||||||
|
|
@ -351,15 +358,15 @@ impl App {
|
||||||
was_open
|
was_open
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(&mut self) -> Command<Message> {
|
fn load(&mut self) -> Task<Message> {
|
||||||
if self.close() {
|
if self.close() {
|
||||||
// Allow a redraw before trying to load again, to prevent deadlock
|
// Allow a redraw before trying to load again, to prevent deadlock
|
||||||
return Command::perform(async { message::app(Message::Reload) }, |x| x);
|
return Task::perform(async { action::app(Message::Reload) }, |x| x);
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = match &self.flags.url_opt {
|
let url = match &self.flags.url_opt {
|
||||||
Some(some) => some.clone(),
|
Some(some) => some.clone(),
|
||||||
None => return Command::none(),
|
None => return Task::none(),
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("Loading {}", url);
|
log::info!("Loading {}", url);
|
||||||
|
|
@ -607,8 +614,8 @@ impl App {
|
||||||
self.update_mpris_state();
|
self.update_mpris_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_config(&mut self) -> Command<Message> {
|
fn update_config(&mut self) -> Task<Message> {
|
||||||
cosmic::app::command::set_theme(self.flags.config.app_theme.theme())
|
set_theme(self.flags.config.app_theme.theme())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_flags(&mut self) {
|
fn update_flags(&mut self) {
|
||||||
|
|
@ -800,10 +807,10 @@ impl App {
|
||||||
self.nav_model.activate(active_id);
|
self.nav_model.activate(active_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_title(&mut self) -> Command<Message> {
|
fn update_title(&mut self) -> Task<Message> {
|
||||||
//TODO: filename?
|
//TODO: filename?
|
||||||
let title = "COSMIC Media Player";
|
let title = "COSMIC Media Player";
|
||||||
self.set_window_title(title.to_string())
|
self.set_window_title(title.to_string(), self.core.main_window_id().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allow screen to dim or turn off if there is no input from the user.
|
/// Allow screen to dim or turn off if there is no input from the user.
|
||||||
|
|
@ -850,7 +857,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the application, and optionally emits command on initialize.
|
/// Creates the application, and optionally emits command on initialize.
|
||||||
fn init(mut core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>) {
|
fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>) {
|
||||||
core.window.content_container = false;
|
core.window.content_container = false;
|
||||||
|
|
||||||
#[cfg(feature = "xdg-portal")]
|
#[cfg(feature = "xdg-portal")]
|
||||||
|
|
@ -905,10 +912,13 @@ impl Application for App {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|url| url.to_file_path().ok());
|
.and_then(|url| url.to_file_path().ok());
|
||||||
let command = match (app.flags.urls.take(), maybe_path) {
|
let command = match (app.flags.urls.take(), maybe_path) {
|
||||||
(Some(urls), _) => command::message::app(Message::MultipleLoad(urls)),
|
(Some(urls), _) => {
|
||||||
(None, Some(path)) if path.is_dir() => command::message::app(Message::FolderLoad(path)),
|
cosmic::task::message(cosmic::action::app(Message::MultipleLoad(urls)))
|
||||||
_ => 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.
|
(None, Some(path)) if path.is_dir() => {
|
||||||
|
cosmic::task::message(cosmic::action::app(Message::FolderLoad(path)))
|
||||||
|
}
|
||||||
|
_ => app.load(),
|
||||||
};
|
};
|
||||||
(app, command)
|
(app, command)
|
||||||
}
|
}
|
||||||
|
|
@ -917,15 +927,15 @@ impl Application for App {
|
||||||
Some(&self.nav_model)
|
Some(&self.nav_model)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_escape(&mut self) -> Command<Self::Message> {
|
fn on_escape(&mut self) -> Task<Self::Message> {
|
||||||
if self.fullscreen {
|
if self.fullscreen {
|
||||||
return self.update(Message::Fullscreen);
|
return self.update(Message::Fullscreen);
|
||||||
} else {
|
} else {
|
||||||
Command::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Message> {
|
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Message> {
|
||||||
// Toggle open state and get clone of node data
|
// Toggle open state and get clone of node data
|
||||||
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
|
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
|
||||||
Some(node) => {
|
Some(node) => {
|
||||||
|
|
@ -966,13 +976,13 @@ impl Application for App {
|
||||||
// folder in condensed mode.
|
// folder in condensed mode.
|
||||||
self.core_mut().nav_bar_set_toggled(true);
|
self.core_mut().nav_bar_set_toggled(true);
|
||||||
|
|
||||||
Command::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
ProjectNode::File { path, .. } => match url::Url::from_file_path(&path) {
|
ProjectNode::File { path, .. } => match url::Url::from_file_path(&path) {
|
||||||
Ok(url) => self.update(Message::FileLoad(url)),
|
Ok(url) => self.update(Message::FileLoad(url)),
|
||||||
Err(()) => {
|
Err(()) => {
|
||||||
log::warn!("failed to convert {:?} to url", path);
|
log::warn!("failed to convert {:?} to url", path);
|
||||||
Command::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -984,21 +994,21 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style(&self) -> Option<theme::Application> {
|
fn style(&self) -> Option<cosmic::iced_core::theme::Style> {
|
||||||
// This ensures we have a solid background color even when using no content container
|
// This ensures we have a solid background color even when using no content container
|
||||||
Some(theme::Application::Custom(Box::new(|theme| {
|
|
||||||
iced_style::application::Appearance {
|
let theme = self.core.system_theme();
|
||||||
background_color: theme.cosmic().bg_color().into(),
|
|
||||||
icon_color: theme.cosmic().on_bg_color().into(),
|
Some(cosmic::iced_core::theme::Style {
|
||||||
text_color: theme.cosmic().on_bg_color().into(),
|
background_color: theme.cosmic().bg_color().into(),
|
||||||
}
|
icon_color: theme.cosmic().on_bg_color().into(),
|
||||||
})))
|
text_color: theme.cosmic().on_bg_color().into(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle application events here.
|
/// Handle application events here.
|
||||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
|
||||||
match message {
|
match message {
|
||||||
Message::None => {}
|
|
||||||
Message::Config(config) => {
|
Message::Config(config) => {
|
||||||
if config != self.flags.config {
|
if config != self.flags.config {
|
||||||
log::info!("update config");
|
log::info!("update config");
|
||||||
|
|
@ -1031,17 +1041,17 @@ impl Application for App {
|
||||||
Message::FileOpen => {
|
Message::FileOpen => {
|
||||||
//TODO: embed cosmic-files dialog (after libcosmic rebase works)
|
//TODO: embed cosmic-files dialog (after libcosmic rebase works)
|
||||||
#[cfg(feature = "xdg-portal")]
|
#[cfg(feature = "xdg-portal")]
|
||||||
return Command::perform(
|
return Task::perform(
|
||||||
async move {
|
async move {
|
||||||
let dialog = cosmic::dialog::file_chooser::open::Dialog::new()
|
let dialog = cosmic::dialog::file_chooser::open::Dialog::new()
|
||||||
.title(fl!("open-media"));
|
.title(fl!("open-media"));
|
||||||
match dialog.open_file().await {
|
match dialog.open_file().await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
message::app(Message::FileLoad(response.url().to_owned()))
|
action::app(Message::FileLoad(response.url().to_owned()))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to open file: {}", err);
|
log::warn!("failed to open file: {}", err);
|
||||||
message::none()
|
action::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1095,7 +1105,7 @@ impl Application for App {
|
||||||
Message::FolderOpen => {
|
Message::FolderOpen => {
|
||||||
//TODO: embed cosmic-files dialog (after libcosmic rebase works)
|
//TODO: embed cosmic-files dialog (after libcosmic rebase works)
|
||||||
#[cfg(feature = "xdg-portal")]
|
#[cfg(feature = "xdg-portal")]
|
||||||
return Command::perform(
|
return Task::perform(
|
||||||
async move {
|
async move {
|
||||||
let dialog = cosmic::dialog::file_chooser::open::Dialog::new()
|
let dialog = cosmic::dialog::file_chooser::open::Dialog::new()
|
||||||
.title(fl!("open-media-folder"));
|
.title(fl!("open-media-folder"));
|
||||||
|
|
@ -1103,16 +1113,16 @@ impl Application for App {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
let url = response.url();
|
let url = response.url();
|
||||||
match url.to_file_path() {
|
match url.to_file_path() {
|
||||||
Ok(path) => message::app(Message::FolderLoad(path)),
|
Ok(path) => action::app(Message::FolderLoad(path)),
|
||||||
Err(()) => {
|
Err(()) => {
|
||||||
log::warn!("unsupported folder URL {:?}", url);
|
log::warn!("unsupported folder URL {:?}", url);
|
||||||
message::none()
|
action::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to open folder: {}", err);
|
log::warn!("failed to open folder: {}", err);
|
||||||
message::none()
|
action::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1156,17 +1166,18 @@ impl Application for App {
|
||||||
//TODO: cleanest way to close dropdowns
|
//TODO: cleanest way to close dropdowns
|
||||||
self.dropdown_opt = None;
|
self.dropdown_opt = None;
|
||||||
|
|
||||||
self.fullscreen = !self.fullscreen;
|
if let Some(window_id) = self.core.main_window_id() {
|
||||||
self.core.window.show_headerbar = !self.fullscreen;
|
self.fullscreen = !self.fullscreen;
|
||||||
self.controls = !self.fullscreen;
|
self.core.window.show_headerbar = !self.fullscreen;
|
||||||
return window::change_mode(
|
return set_mode(
|
||||||
window::Id::MAIN,
|
window_id,
|
||||||
if self.fullscreen {
|
if self.fullscreen {
|
||||||
window::Mode::Fullscreen
|
window::Mode::Fullscreen
|
||||||
} else {
|
} else {
|
||||||
window::Mode::Windowed
|
window::Mode::Windowed
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::Key(modifiers, key) => {
|
Message::Key(modifiers, key) => {
|
||||||
for (key_bind, action) in self.key_binds.iter() {
|
for (key_bind, action) in self.key_binds.iter() {
|
||||||
|
|
@ -1337,7 +1348,7 @@ impl Application for App {
|
||||||
if self.flags.config_state.player_state.repeat == RepeatState::Track {
|
if self.flags.config_state.player_state.repeat == RepeatState::Track {
|
||||||
// we hook Message::PlayNext to the EOS signal. iced_video_player always emits EOS regardless of
|
// we hook Message::PlayNext to the EOS signal. iced_video_player always emits EOS regardless of
|
||||||
// looping state, so do nothing if repeat is set.
|
// looping state, so do nothing if repeat is set.
|
||||||
return Command::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
//first we get info about current media id & position in nav_bar
|
//first we get info about current media id & position in nav_bar
|
||||||
|
|
@ -1385,7 +1396,7 @@ impl Application for App {
|
||||||
if let Some(video) = &mut self.video_opt {
|
if let Some(video) = &mut self.video_opt {
|
||||||
video.set_paused(true);
|
video.set_paused(true);
|
||||||
}
|
}
|
||||||
return Command::perform(
|
return Task::perform(
|
||||||
async move {
|
async move {
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
match gst_pbutils::MissingPluginMessage::parse(&element) {
|
match gst_pbutils::MissingPluginMessage::parse(&element) {
|
||||||
|
|
@ -1419,7 +1430,7 @@ impl Application for App {
|
||||||
"gstreamer registry update: {:?}",
|
"gstreamer registry update: {:?}",
|
||||||
gst::Registry::update()
|
gst::Registry::update()
|
||||||
);
|
);
|
||||||
return message::app(Message::Reload);
|
return action::app(Message::Reload);
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
log::warn!("failed to install plugins: {status}");
|
log::warn!("failed to install plugins: {status}");
|
||||||
|
|
@ -1433,7 +1444,7 @@ impl Application for App {
|
||||||
log::warn!("failed to parse missing plugin message: {err}");
|
log::warn!("failed to parse missing plugin message: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message::none()
|
action::none()
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
@ -1467,7 +1478,7 @@ impl Application for App {
|
||||||
process::exit(0);
|
process::exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
|
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
|
||||||
|
|
@ -1501,25 +1512,25 @@ impl Application for App {
|
||||||
let Some(video) = &self.video_opt else {
|
let Some(video) = &self.video_opt else {
|
||||||
//TODO: use space variables
|
//TODO: use space variables
|
||||||
let column = widget::column::with_capacity(4)
|
let column = widget::column::with_capacity(4)
|
||||||
.align_items(Alignment::Center)
|
.align_x(Alignment::Center)
|
||||||
.spacing(24)
|
.spacing(24)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.push(widget::vertical_space(Length::Fill))
|
.push(widget::space::vertical())
|
||||||
.push(
|
.push(
|
||||||
widget::column::with_capacity(2)
|
widget::column::with_capacity(2)
|
||||||
.align_items(Alignment::Center)
|
.align_x(Alignment::Center)
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.push(widget::icon::from_name("folder-symbolic").size(64))
|
.push(widget::icon::from_name("folder-symbolic").size(64))
|
||||||
.push(widget::text::body(fl!("no-video-or-audio-file-open"))),
|
.push(widget::text::body(fl!("no-video-or-audio-file-open"))),
|
||||||
)
|
)
|
||||||
.push(widget::button::suggested(fl!("open-file")).on_press(Message::FileOpen))
|
.push(widget::button::suggested(fl!("open-file")).on_press(Message::FileOpen))
|
||||||
.push(widget::vertical_space(Length::Fill));
|
.push(widget::space::vertical());
|
||||||
|
|
||||||
return widget::container(column)
|
return widget::container(column)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.style(theme::Container::WindowBackground)
|
.class(theme::Container::WindowBackground)
|
||||||
.into();
|
.into();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1534,6 +1545,7 @@ impl Application for App {
|
||||||
.on_new_frame(Message::NewFrame)
|
.on_new_frame(Message::NewFrame)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
.id(cosmic::widget::Id::new("video-player".to_string()))
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let mut background_color = Color::BLACK;
|
let mut background_color = Color::BLACK;
|
||||||
|
|
@ -1543,7 +1555,7 @@ impl Application for App {
|
||||||
text_color_opt = Some(Color::from(theme.cosmic().on_bg_component_color()));
|
text_color_opt = Some(Color::from(theme.cosmic().on_bg_component_color()));
|
||||||
|
|
||||||
let mut col = widget::column();
|
let mut col = widget::column();
|
||||||
col = col.push(widget::vertical_space(Length::Fill));
|
col = col.push(widget::space::vertical());
|
||||||
if let Some(album_art) = &self.album_art_opt {
|
if let Some(album_art) = &self.album_art_opt {
|
||||||
col = col.push(
|
col = col.push(
|
||||||
widget::image(widget::image::Handle::from_path(album_art.path()))
|
widget::image(widget::image::Handle::from_path(album_art.path()))
|
||||||
|
|
@ -1553,7 +1565,7 @@ impl Application for App {
|
||||||
} else {
|
} else {
|
||||||
col = col.push(widget::icon::from_name("audio-x-generic-symbolic").size(256));
|
col = col.push(widget::icon::from_name("audio-x-generic-symbolic").size(256));
|
||||||
}
|
}
|
||||||
col = col.push(widget::vertical_space(space_s));
|
col = col.push(widget::space::vertical().height(space_s));
|
||||||
if self.mpris_meta.title.is_empty() {
|
if self.mpris_meta.title.is_empty() {
|
||||||
col = col.push(widget::text::title4(fl!("untitled")));
|
col = col.push(widget::text::title4(fl!("untitled")));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1566,7 +1578,7 @@ impl Application for App {
|
||||||
col = col.push(widget::text::body(artist));
|
col = col.push(widget::text::body(artist));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
col = col.push(widget::vertical_space(space_s));
|
col = col.push(widget::space::vertical().height(space_s));
|
||||||
if !self.mpris_meta.album.is_empty() {
|
if !self.mpris_meta.album.is_empty() {
|
||||||
col = col.push(widget::text::body(fl!(
|
col = col.push(widget::text::body(fl!(
|
||||||
"album",
|
"album",
|
||||||
|
|
@ -1576,7 +1588,7 @@ impl Application for App {
|
||||||
if let Some(year) = &self.mpris_meta.album_year_opt {
|
if let Some(year) = &self.mpris_meta.album_year_opt {
|
||||||
col = col.push(widget::text::body(format!("{}", year)));
|
col = col.push(widget::text::body(format!("{}", year)));
|
||||||
}
|
}
|
||||||
col = col.push(widget::vertical_space(Length::Fill));
|
col = col.push(widget::space::vertical());
|
||||||
|
|
||||||
// Space to keep from going under control overlay
|
// Space to keep from going under control overlay
|
||||||
let mut control_height = space_xxs + 32 + space_xxs;
|
let mut control_height = space_xxs + 32 + space_xxs;
|
||||||
|
|
@ -1586,11 +1598,11 @@ impl Application for App {
|
||||||
|
|
||||||
// This is a hack to have the video player running but not visible (since the controls will cover it as an overlay)
|
// This is a hack to have the video player running but not visible (since the controls will cover it as an overlay)
|
||||||
video_player = widget::row::with_children(vec![
|
video_player = widget::row::with_children(vec![
|
||||||
widget::horizontal_space(Length::Fill).into(),
|
widget::space::horizontal().into(),
|
||||||
widget::container(col.push(widget::container(video_player).height(control_height)))
|
widget::container(col.push(widget::container(video_player).height(control_height)))
|
||||||
.width(320)
|
.width(320)
|
||||||
.into(),
|
.into(),
|
||||||
widget::horizontal_space(Length::Fill).into(),
|
widget::space::horizontal().into(),
|
||||||
])
|
])
|
||||||
.into();
|
.into();
|
||||||
}
|
}
|
||||||
|
|
@ -1629,7 +1641,7 @@ impl Application for App {
|
||||||
.step(0.01)
|
.step(0.01)
|
||||||
.into(),
|
.into(),
|
||||||
])
|
])
|
||||||
.align_items(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1668,14 +1680,14 @@ impl Application for App {
|
||||||
|
|
||||||
popup_items.push(
|
popup_items.push(
|
||||||
widget::row::with_children(vec![
|
widget::row::with_children(vec![
|
||||||
widget::horizontal_space(Length::Fill).into(),
|
widget::space::horizontal().into(),
|
||||||
widget::container(column)
|
widget::container(column)
|
||||||
.padding(1)
|
.padding(1)
|
||||||
//TODO: move style to libcosmic
|
//TODO: move style to libcosmic
|
||||||
.style(theme::Container::custom(|theme| {
|
.class(theme::Container::custom(|theme| {
|
||||||
let cosmic = theme.cosmic();
|
let cosmic = theme.cosmic();
|
||||||
let component = &cosmic.background.component;
|
let component = &cosmic.background.component;
|
||||||
widget::container::Appearance {
|
widget::container::Style {
|
||||||
icon_color: Some(component.on.into()),
|
icon_color: Some(component.on.into()),
|
||||||
text_color: Some(component.on.into()),
|
text_color: Some(component.on.into()),
|
||||||
background: Some(Background::Color(component.base.into())),
|
background: Some(Background::Color(component.base.into())),
|
||||||
|
|
@ -1694,8 +1706,8 @@ impl Application for App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if self.controls {
|
if self.controls {
|
||||||
let mut row = widget::row::with_capacity(8)
|
let mut row = widget::row::with_capacity(7)
|
||||||
.align_items(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.spacing(space_xxs)
|
.spacing(space_xxs)
|
||||||
.push(
|
.push(
|
||||||
widget::button::icon(
|
widget::button::icon(
|
||||||
|
|
@ -1721,14 +1733,14 @@ impl Application for App {
|
||||||
RepeatState::Track => RepeatState::Disabled,
|
RepeatState::Track => RepeatState::Disabled,
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
match self.flags.config_state.player_state.repeat {
|
widget::text(match self.flags.config_state.player_state.repeat {
|
||||||
RepeatState::Disabled => fl!("repeat-disabled"),
|
RepeatState::Disabled => fl!("repeat-disabled"),
|
||||||
RepeatState::Track => fl!("repeat-track"),
|
RepeatState::Track => fl!("repeat-track"),
|
||||||
},
|
}),
|
||||||
widget::tooltip::Position::Top,
|
widget::tooltip::Position::Top,
|
||||||
));
|
));
|
||||||
if self.core.is_condensed() {
|
if self.core.is_condensed() {
|
||||||
row = row.push(widget::horizontal_space(Length::Fill));
|
row = row.push(widget::space::horizontal());
|
||||||
} else {
|
} else {
|
||||||
row = row
|
row = row
|
||||||
.push(widget::text(format_time(self.position)).font(font::mono()))
|
.push(widget::text(format_time(self.position)).font(font::mono()))
|
||||||
|
|
@ -1777,7 +1789,7 @@ impl Application for App {
|
||||||
popup_items.push(
|
popup_items.push(
|
||||||
widget::container(row)
|
widget::container(row)
|
||||||
.padding([space_xxs, space_xs])
|
.padding([space_xxs, space_xs])
|
||||||
.style(theme::Container::WindowBackground)
|
.class(theme::Container::WindowBackground)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1785,7 +1797,7 @@ impl Application for App {
|
||||||
popup_items.push(
|
popup_items.push(
|
||||||
widget::container(
|
widget::container(
|
||||||
widget::row::with_capacity(3)
|
widget::row::with_capacity(3)
|
||||||
.align_items(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.spacing(space_xxs)
|
.spacing(space_xxs)
|
||||||
.push(widget::text(format_time(self.position)).font(font::mono()))
|
.push(widget::text(format_time(self.position)).font(font::mono()))
|
||||||
.push(
|
.push(
|
||||||
|
|
@ -1799,7 +1811,7 @@ impl Application for App {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.padding([space_xxs, space_xs])
|
.padding([space_xxs, space_xs])
|
||||||
.style(theme::Container::WindowBackground)
|
.class(theme::Container::WindowBackground)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1811,9 +1823,9 @@ impl Application for App {
|
||||||
widget::container(popover)
|
widget::container(popover)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.style(theme::Container::Custom(Box::new(move |_theme| {
|
.class(theme::Container::Custom(Box::new(move |_theme| {
|
||||||
let mut appearance =
|
let mut appearance =
|
||||||
widget::container::Appearance::default().with_background(background_color);
|
widget::container::Style::default().background(background_color);
|
||||||
if let Some(text_color) = text_color_opt {
|
if let Some(text_color) = text_color_opt {
|
||||||
appearance.text_color = Some(text_color);
|
appearance.text_color = Some(text_color);
|
||||||
}
|
}
|
||||||
|
|
@ -1828,7 +1840,7 @@ impl Application for App {
|
||||||
struct ThemeSubscription;
|
struct ThemeSubscription;
|
||||||
|
|
||||||
let mut subscriptions = vec![
|
let mut subscriptions = vec![
|
||||||
event::listen_with(|event, _status| match event {
|
event::listen_with(|event, _status, _window_id| match event {
|
||||||
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => {
|
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => {
|
||||||
Some(Message::Key(modifiers, key))
|
Some(Message::Key(modifiers, key))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
src/menu.rs
23
src/menu.rs
|
|
@ -1,8 +1,12 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
Element, theme,
|
theme,
|
||||||
widget::menu::{self, ItemHeight, ItemWidth, MenuBar, key_bind::KeyBind},
|
widget::{
|
||||||
|
menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar},
|
||||||
|
RcElementWrapper,
|
||||||
|
},
|
||||||
|
Element,
|
||||||
};
|
};
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
|
@ -39,6 +43,7 @@ pub fn menu_bar<'a>(
|
||||||
for (i, path) in config_state.recent_files.iter().enumerate() {
|
for (i, path) in config_state.recent_files.iter().enumerate() {
|
||||||
recent_files.push(menu::Item::Button(
|
recent_files.push(menu::Item::Button(
|
||||||
format_url(path),
|
format_url(path),
|
||||||
|
None,
|
||||||
Action::FileOpenRecent(i),
|
Action::FileOpenRecent(i),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +51,7 @@ pub fn menu_bar<'a>(
|
||||||
recent_files.push(menu::Item::Divider);
|
recent_files.push(menu::Item::Divider);
|
||||||
recent_files.push(menu::Item::Button(
|
recent_files.push(menu::Item::Button(
|
||||||
fl!("clear-recent"),
|
fl!("clear-recent"),
|
||||||
|
None,
|
||||||
Action::FileClearRecents,
|
Action::FileClearRecents,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -59,6 +65,7 @@ pub fn menu_bar<'a>(
|
||||||
for (i, path) in config_state.recent_projects.iter().enumerate() {
|
for (i, path) in config_state.recent_projects.iter().enumerate() {
|
||||||
recent_projects.push(menu::Item::Button(
|
recent_projects.push(menu::Item::Button(
|
||||||
format_path(path),
|
format_path(path),
|
||||||
|
None,
|
||||||
Action::FolderOpenRecent(i),
|
Action::FolderOpenRecent(i),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +73,7 @@ pub fn menu_bar<'a>(
|
||||||
recent_projects.push(menu::Item::Divider);
|
recent_projects.push(menu::Item::Divider);
|
||||||
recent_projects.push(menu::Item::Button(
|
recent_projects.push(menu::Item::Button(
|
||||||
fl!("clear-recent"),
|
fl!("clear-recent"),
|
||||||
|
None,
|
||||||
Action::FolderClearRecents,
|
Action::FolderClearRecents,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -74,24 +82,25 @@ pub fn menu_bar<'a>(
|
||||||
for (folder_i, (name, _path)) in projects.iter().enumerate() {
|
for (folder_i, (name, _path)) in projects.iter().enumerate() {
|
||||||
close_projects.push(menu::Item::Button(
|
close_projects.push(menu::Item::Button(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
|
None,
|
||||||
Action::FolderClose(folder_i),
|
Action::FolderClose(folder_i),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuBar::new(vec![menu::Tree::with_children(
|
MenuBar::new(vec![menu::Tree::with_children(
|
||||||
menu::root(fl!("file")),
|
RcElementWrapper::new(Element::from(menu::root(fl!("file")))),
|
||||||
menu::items(
|
menu::items(
|
||||||
key_binds,
|
key_binds,
|
||||||
vec![
|
vec![
|
||||||
menu::Item::Button(fl!("open-media"), Action::FileOpen),
|
menu::Item::Button(fl!("open-media"), None, Action::FileOpen),
|
||||||
menu::Item::Folder(fl!("open-recent-media"), recent_files),
|
menu::Item::Folder(fl!("open-recent-media"), recent_files),
|
||||||
menu::Item::Button(fl!("close-file"), Action::FileClose),
|
menu::Item::Button(fl!("close-file"), None, Action::FileClose),
|
||||||
menu::Item::Divider,
|
menu::Item::Divider,
|
||||||
menu::Item::Button(fl!("open-media-folder"), Action::FolderOpen),
|
menu::Item::Button(fl!("open-media-folder"), None, Action::FolderOpen),
|
||||||
menu::Item::Folder(fl!("open-recent-media-folder"), recent_projects),
|
menu::Item::Folder(fl!("open-recent-media-folder"), recent_projects),
|
||||||
menu::Item::Folder(fl!("close-media-folder"), close_projects),
|
menu::Item::Folder(fl!("close-media-folder"), close_projects),
|
||||||
menu::Item::Divider,
|
menu::Item::Divider,
|
||||||
menu::Item::Button(fl!("quit"), Action::WindowClose),
|
menu::Item::Button(fl!("quit"), None, Action::WindowClose),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)])
|
)])
|
||||||
|
|
|
||||||
29
src/mpris.rs
29
src/mpris.rs
|
|
@ -1,6 +1,9 @@
|
||||||
use cosmic::iced::{
|
use cosmic::{
|
||||||
futures::{self, SinkExt},
|
iced::{
|
||||||
subscription::{self, Subscription},
|
futures::{self, SinkExt, Stream},
|
||||||
|
Subscription,
|
||||||
|
},
|
||||||
|
iced_futures::stream,
|
||||||
};
|
};
|
||||||
use mpris_server::{
|
use mpris_server::{
|
||||||
LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface,
|
LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface,
|
||||||
|
|
@ -377,15 +380,13 @@ impl PlaylistsInterface for Player {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pub fn subscription() -> Subscription<Message> {
|
fn watcher_stream() -> impl Stream<Item = Message> {
|
||||||
struct MprisSubscription;
|
stream::channel(
|
||||||
subscription::channel(
|
5,
|
||||||
TypeId::of::<MprisSubscription>(),
|
move |mut msg_tx: futures::channel::mpsc::Sender<Message>| async move {
|
||||||
16,
|
|
||||||
move |mut msg_tx| async move {
|
|
||||||
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
|
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
|
||||||
let meta = MprisMeta::default();
|
let meta: MprisMeta = MprisMeta::default();
|
||||||
let state = MprisState::default();
|
let state: MprisState = MprisState::default();
|
||||||
msg_tx
|
msg_tx
|
||||||
.send(Message::MprisChannel(meta.clone(), state.clone(), event_tx))
|
.send(Message::MprisChannel(meta.clone(), state.clone(), event_tx))
|
||||||
.await
|
.await
|
||||||
|
|
@ -455,3 +456,9 @@ pub fn subscription() -> Subscription<Message> {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
|
pub fn subscription() -> Subscription<Message> {
|
||||||
|
struct MprisSubscription;
|
||||||
|
Subscription::run_with(TypeId::of::<MprisSubscription>(), |_| watcher_stream())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use cosmic::iced_core::image::Data;
|
|
||||||
use iced_video_player::Position;
|
use iced_video_player::Position;
|
||||||
use image::{DynamicImage, ImageFormat, RgbaImage};
|
use image::{DynamicImage, ImageFormat, RgbaImage};
|
||||||
use std::{error::Error, num::NonZero, path::Path, time::Duration};
|
use std::{error::Error, num::NonZero, path::Path, time::Duration};
|
||||||
|
|
@ -29,9 +28,10 @@ pub fn main(
|
||||||
};
|
};
|
||||||
video.thumbnails([position], NonZero::new(1).unwrap())?
|
video.thumbnails([position], NonZero::new(1).unwrap())?
|
||||||
};
|
};
|
||||||
//TODO: do not require clone of pixels data
|
// TODO: do not require clone of pixels data
|
||||||
match thumbnails[0].data() {
|
match &thumbnails[0] {
|
||||||
Data::Rgba {
|
cosmic::widget::image::Handle::Rgba {
|
||||||
|
id: _,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
pixels,
|
pixels,
|
||||||
|
|
|
||||||
10
src/video.rs
10
src/video.rs
|
|
@ -4,7 +4,7 @@ use iced_video_player::{
|
||||||
gst_app, gst_pbutils,
|
gst_app, gst_pbutils,
|
||||||
};
|
};
|
||||||
|
|
||||||
use cosmic::app::{Command, message};
|
use cosmic::{action, app::Task};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct VideoSettings {
|
pub struct VideoSettings {
|
||||||
|
|
@ -14,7 +14,7 @@ pub struct VideoSettings {
|
||||||
pub fn new_video(
|
pub fn new_video(
|
||||||
url: &url::Url,
|
url: &url::Url,
|
||||||
settings: VideoSettings,
|
settings: VideoSettings,
|
||||||
) -> Result<Video, cosmic::Command<cosmic::app::Message<super::Message>>> {
|
) -> Result<Video, cosmic::Task<cosmic::Action<super::Message>>> {
|
||||||
//TODO: this code came from iced_video_player::Video::new and has been modified to stop the pipeline on error
|
//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.
|
//TODO: remove unwraps and enable playback of files with only audio.
|
||||||
gst::init().unwrap();
|
gst::init().unwrap();
|
||||||
|
|
@ -68,8 +68,8 @@ pub fn new_video(
|
||||||
match msg.view() {
|
match msg.view() {
|
||||||
gst::MessageView::Element(element) => {
|
gst::MessageView::Element(element) => {
|
||||||
if gst_pbutils::MissingPluginMessage::is(&element) {
|
if gst_pbutils::MissingPluginMessage::is(&element) {
|
||||||
commands.push(Command::perform(
|
commands.push(Task::perform(
|
||||||
async { message::app(super::Message::MissingPlugin(msg)) },
|
async { action::app(super::Message::MissingPlugin(msg)) },
|
||||||
|x| x,
|
|x| x,
|
||||||
));
|
));
|
||||||
// Do one codec install at a time
|
// Do one codec install at a time
|
||||||
|
|
@ -80,7 +80,7 @@ pub fn new_video(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pipeline.set_state(gst::State::Null).unwrap();
|
pipeline.set_state(gst::State::Null).unwrap();
|
||||||
Err(Command::batch(commands))
|
Err(Task::batch(commands))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue