Add file menu, improve naming of subtitles
This commit is contained in:
parent
214e894491
commit
7f9d56ae0c
6 changed files with 210 additions and 72 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
|
@ -1071,6 +1071,7 @@ name = "cosmic-player"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"gstreamer-tag",
|
||||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
"iced_video_player",
|
||||
|
|
@ -1096,7 +1097,7 @@ dependencies = [
|
|||
"rayon",
|
||||
"rustc-hash",
|
||||
"rustybuzz 0.14.1",
|
||||
"self_cell 1.0.4",
|
||||
"self_cell 1.1.0",
|
||||
"smol_str",
|
||||
"swash",
|
||||
"sys-locale",
|
||||
|
|
@ -2301,6 +2302,31 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gstreamer-tag"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "089398382ac684c23c676a2c17c3da611e58ac022e9d6b6ed225eab5b5a97c24"
|
||||
dependencies = [
|
||||
"glib",
|
||||
"gstreamer",
|
||||
"gstreamer-tag-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gstreamer-tag-sys"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c554d84f8e29aa2bae07b1ce00a79d2ea0a8e38b55c5cacc4411221955c0dbe1"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gstreamer-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gstreamer-video"
|
||||
version = "0.23.3"
|
||||
|
|
@ -2670,7 +2696,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_video_player"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/jackpot51/iced_video_player.git?branch=prev-cosmic#cb70dc44be8d04b323d749322d06a2dd8a659e68"
|
||||
source = "git+https://github.com/jackpot51/iced_video_player.git?branch=prev-cosmic#c6adbcd70c518ad6ae418f4e6f42ce68df586001"
|
||||
dependencies = [
|
||||
"glib",
|
||||
"gstreamer",
|
||||
|
|
@ -4560,14 +4586,14 @@ version = "0.10.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d"
|
||||
dependencies = [
|
||||
"self_cell 1.0.4",
|
||||
"self_cell 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.0.4"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a"
|
||||
checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
gstreamer-tag = "0.23"
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["serde_derive"] }
|
||||
tokio = "1"
|
||||
|
|
@ -32,7 +33,8 @@ version = "0.2.1"
|
|||
features = ["serde"]
|
||||
|
||||
[features]
|
||||
default = ["wgpu"]
|
||||
default = ["xdg-portal", "wgpu"]
|
||||
xdg-portal = ["libcosmic/xdg-portal"]
|
||||
wgpu = ["iced_video_player/wgpu", "libcosmic/wgpu"]
|
||||
|
||||
[profile.release-with-debug]
|
||||
|
|
|
|||
|
|
@ -9,3 +9,12 @@ theme = Theme
|
|||
match-desktop = Match desktop
|
||||
dark = Dark
|
||||
light = Light
|
||||
|
||||
# Menu
|
||||
|
||||
## File
|
||||
file = File
|
||||
open-media = Open media...
|
||||
open-recent-media = Open recent media
|
||||
close-file = Close file
|
||||
quit = Quit
|
||||
|
|
@ -1,48 +1,9 @@
|
|||
use cosmic::{
|
||||
iced::keyboard::{Key, Modifiers},
|
||||
iced_core::keyboard::key::Named,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, fmt};
|
||||
use cosmic::{iced::keyboard::Key, iced_core::keyboard::key::Named};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::Action;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub enum Modifier {
|
||||
Super,
|
||||
Ctrl,
|
||||
Alt,
|
||||
Shift,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct KeyBind {
|
||||
pub modifiers: Vec<Modifier>,
|
||||
pub key: Key,
|
||||
}
|
||||
|
||||
impl KeyBind {
|
||||
pub fn matches(&self, modifiers: Modifiers, key: &Key) -> bool {
|
||||
key == &self.key
|
||||
&& modifiers.logo() == self.modifiers.contains(&Modifier::Super)
|
||||
&& modifiers.control() == self.modifiers.contains(&Modifier::Ctrl)
|
||||
&& modifiers.alt() == self.modifiers.contains(&Modifier::Alt)
|
||||
&& modifiers.shift() == self.modifiers.contains(&Modifier::Shift)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for KeyBind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for modifier in self.modifiers.iter() {
|
||||
write!(f, "{:?} + ", modifier)?;
|
||||
}
|
||||
match &self.key {
|
||||
Key::Character(c) => write!(f, "{}", c.to_uppercase()),
|
||||
Key::Named(named) => write!(f, "{:?}", named),
|
||||
other => write!(f, "{:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use cosmic::widget::menu::key_bind::{KeyBind, Modifier};
|
||||
|
||||
//TODO: load from config
|
||||
pub fn key_binds() -> HashMap<KeyBind, Action> {
|
||||
|
|
|
|||
155
src/main.rs
155
src/main.rs
|
|
@ -13,17 +13,18 @@ use cosmic::{
|
|||
window, Alignment, Color, Length, Limits,
|
||||
},
|
||||
theme,
|
||||
widget::{self, Slider},
|
||||
widget::{self, menu::action::MenuAction, Slider},
|
||||
Application, ApplicationExt, Element,
|
||||
};
|
||||
use iced_video_player::{
|
||||
gst::{self, prelude::*},
|
||||
gst_pbutils, Video, VideoPlayer,
|
||||
gst_app, gst_pbutils, Video, VideoPlayer,
|
||||
};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
fs,
|
||||
ffi::{CStr, CString},
|
||||
fs, process,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
|
|
@ -35,6 +36,7 @@ use crate::{
|
|||
mod config;
|
||||
mod key_bind;
|
||||
mod localize;
|
||||
mod menu;
|
||||
|
||||
static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0);
|
||||
|
||||
|
|
@ -42,6 +44,20 @@ const GST_PLAY_FLAG_VIDEO: i32 = 1 << 0;
|
|||
const GST_PLAY_FLAG_AUDIO: i32 = 1 << 1;
|
||||
const GST_PLAY_FLAG_TEXT: i32 = 1 << 2;
|
||||
|
||||
fn language_name(code: &str) -> Option<String> {
|
||||
let code_c = CString::new(code).ok()?;
|
||||
let name_c = unsafe {
|
||||
//TODO: export this in gstreamer_tag
|
||||
let name_ptr = gstreamer_tag::ffi::gst_tag_get_language_name(code_c.as_ptr());
|
||||
if name_ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
CStr::from_ptr(name_ptr)
|
||||
};
|
||||
let name = name_c.to_str().ok()?;
|
||||
Some(name.to_string())
|
||||
}
|
||||
|
||||
/// Runs application with these settings
|
||||
#[rustfmt::skip]
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
|
@ -102,19 +118,27 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Action {
|
||||
FileClose,
|
||||
FileOpen,
|
||||
Fullscreen,
|
||||
PlayPause,
|
||||
SeekBackward,
|
||||
SeekForward,
|
||||
WindowClose,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn message(&self) -> Message {
|
||||
impl MenuAction for Action {
|
||||
type Message = Message;
|
||||
|
||||
fn message(&self) -> Message {
|
||||
match self {
|
||||
Self::FileClose => Message::FileClose,
|
||||
Self::FileOpen => Message::FileOpen,
|
||||
Self::Fullscreen => Message::Fullscreen,
|
||||
Self::PlayPause => Message::PlayPause,
|
||||
Self::SeekBackward => Message::SeekRelative(-10.0),
|
||||
Self::SeekForward => Message::SeekRelative(10.0),
|
||||
Self::WindowClose => Message::WindowClose,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,6 +154,9 @@ pub struct Flags {
|
|||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
Config(Config),
|
||||
FileClose,
|
||||
FileLoad(url::Url),
|
||||
FileOpen,
|
||||
Fullscreen,
|
||||
Key(Modifiers, Key),
|
||||
AudioCode(usize),
|
||||
|
|
@ -144,6 +171,7 @@ pub enum Message {
|
|||
Reload,
|
||||
ShowControls,
|
||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||
WindowClose,
|
||||
}
|
||||
|
||||
/// The [`App`] stores application-specific state.
|
||||
|
|
@ -166,7 +194,14 @@ pub struct App {
|
|||
|
||||
impl App {
|
||||
fn close(&mut self) {
|
||||
self.video_opt = None;
|
||||
//TODO: drop does not work well
|
||||
if let Some(mut video) = self.video_opt.take() {
|
||||
log::info!("pausing video");
|
||||
video.set_paused(true);
|
||||
log::info!("dropping video");
|
||||
drop(video);
|
||||
log::info!("dropped video");
|
||||
}
|
||||
self.position = 0.0;
|
||||
self.duration = 0.0;
|
||||
self.dragging = false;
|
||||
|
|
@ -184,13 +219,44 @@ impl App {
|
|||
None => return Command::none(),
|
||||
};
|
||||
|
||||
let video = match Video::new(&url) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::warn!("failed to open {:?}: {err}", url);
|
||||
return Command::none();
|
||||
log::info!("Loading {}", url);
|
||||
|
||||
//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.
|
||||
let video = {
|
||||
gst::init().unwrap();
|
||||
|
||||
let pipeline = format!(
|
||||
"playbin uri=\"{}\" video-sink=\"videoscale ! videoconvert ! appsink name=iced_video drop=true caps=video/x-raw,format=NV12,pixel-aspect-ratio=1/1\"",
|
||||
url.as_str()
|
||||
);
|
||||
let pipeline = gst::parse::launch(pipeline.as_ref())
|
||||
.unwrap()
|
||||
.downcast::<gst::Pipeline>()
|
||||
.map_err(|_| iced_video_player::Error::Cast)
|
||||
.unwrap();
|
||||
|
||||
let video_sink: gst::Element = pipeline.property("video-sink");
|
||||
let pad = video_sink.pads().first().cloned().unwrap();
|
||||
let pad = pad.dynamic_cast::<gst::GhostPad>().unwrap();
|
||||
let bin = pad
|
||||
.parent_element()
|
||||
.unwrap()
|
||||
.downcast::<gst::Bin>()
|
||||
.unwrap();
|
||||
let video_sink = bin.by_name("iced_video").unwrap();
|
||||
let video_sink = video_sink.downcast::<gst_app::AppSink>().unwrap();
|
||||
|
||||
match Video::from_gst_pipeline(pipeline.clone(), video_sink, None) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::warn!("failed to open {}: {err}", url);
|
||||
pipeline.set_state(gst::State::Null).unwrap();
|
||||
return Command::none();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.duration = video.duration().as_secs_f64();
|
||||
let pipeline = video.pipeline();
|
||||
self.video_opt = Some(video);
|
||||
|
|
@ -200,13 +266,15 @@ impl App {
|
|||
for i in 0..n_audio {
|
||||
let tags: gst::TagList = pipeline.emit_by_name("get-audio-tags", &[&i]);
|
||||
log::info!("audio stream {i}: {tags:?}");
|
||||
self.audio_codes.push(
|
||||
if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
|
||||
language_code.get().to_string()
|
||||
self.audio_codes
|
||||
.push(if let Some(title) = tags.get::<gst::tags::Title>() {
|
||||
title.get().to_string()
|
||||
} else if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
|
||||
let language_code = language_code.get();
|
||||
language_name(language_code).unwrap_or_else(|| language_code.to_string())
|
||||
} else {
|
||||
format!("Audio #{i}")
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
self.current_audio = pipeline.property::<i32>("current-audio");
|
||||
|
||||
|
|
@ -215,13 +283,15 @@ impl App {
|
|||
for i in 0..n_text {
|
||||
let tags: gst::TagList = pipeline.emit_by_name("get-text-tags", &[&i]);
|
||||
log::info!("text stream {i}: {tags:?}");
|
||||
self.text_codes.push(
|
||||
if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
|
||||
language_code.get().to_string()
|
||||
self.text_codes
|
||||
.push(if let Some(title) = tags.get::<gst::tags::Title>() {
|
||||
title.get().to_string()
|
||||
} else if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
|
||||
let language_code = language_code.get();
|
||||
language_name(language_code).unwrap_or_else(|| language_code.to_string())
|
||||
} else {
|
||||
format!("Subtitle #{i}")
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
self.current_text = pipeline.property::<i32>("current-text");
|
||||
|
||||
|
|
@ -337,6 +407,33 @@ impl Application for App {
|
|||
return self.update_config();
|
||||
}
|
||||
}
|
||||
Message::FileClose => {
|
||||
self.close();
|
||||
}
|
||||
Message::FileLoad(url) => {
|
||||
self.flags.url_opt = Some(url);
|
||||
return self.load();
|
||||
}
|
||||
Message::FileOpen => {
|
||||
//TODO: embed cosmic-files dialog (after libcosmic rebase works)
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
return Command::perform(
|
||||
async move {
|
||||
let dialog = cosmic::dialog::file_chooser::open::Dialog::new()
|
||||
.title(fl!("open-media"));
|
||||
match dialog.open_file().await {
|
||||
Ok(response) => {
|
||||
message::app(Message::FileLoad(response.url().to_owned()))
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to open file: {}", err);
|
||||
message::none()
|
||||
}
|
||||
}
|
||||
},
|
||||
|x| x,
|
||||
);
|
||||
}
|
||||
Message::Fullscreen => {
|
||||
self.fullscreen = !self.fullscreen;
|
||||
self.core.window.show_headerbar = !self.fullscreen;
|
||||
|
|
@ -463,14 +560,18 @@ impl Application for App {
|
|||
Message::SystemThemeModeChange(_theme_mode) => {
|
||||
return self.update_config();
|
||||
}
|
||||
Message::WindowClose => {
|
||||
process::exit(0);
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
||||
let mut row = widget::row::with_capacity(4)
|
||||
let mut row = widget::row::with_capacity(5)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8);
|
||||
row = row.push(menu::menu_bar(&self.flags.config, &self.key_binds));
|
||||
if !self.audio_codes.is_empty() {
|
||||
//TODO: allow mute/unmute/change volume
|
||||
row = row.push(widget::icon::from_name("audio-volume-high-symbolic").size(16));
|
||||
|
|
@ -494,6 +595,12 @@ impl Application for App {
|
|||
|
||||
/// Creates a view after each update.
|
||||
fn view(&self) -> Element<Self::Message> {
|
||||
let cosmic_theme::Spacing {
|
||||
space_xxs,
|
||||
space_xs,
|
||||
..
|
||||
} = theme::active().cosmic().spacing;
|
||||
|
||||
let format_time = |time_float: f64| -> String {
|
||||
let time = time_float.floor() as i64;
|
||||
let seconds = time % 60;
|
||||
|
|
@ -527,8 +634,7 @@ impl Application for App {
|
|||
widget::container(
|
||||
widget::row::with_capacity(5)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
.padding([0, 8])
|
||||
.spacing(space_xxs)
|
||||
.push(
|
||||
widget::button::icon(
|
||||
if self.video_opt.as_ref().map_or(true, |video| video.paused()) {
|
||||
|
|
@ -558,6 +664,7 @@ impl Application for App {
|
|||
.on_press(Message::Fullscreen),
|
||||
),
|
||||
)
|
||||
.padding([space_xxs, space_xs])
|
||||
.style(theme::Container::WindowBackground),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
33
src/menu.rs
Normal file
33
src/menu.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::widget::menu::key_bind::KeyBind;
|
||||
use cosmic::widget::menu::{items as menu_items, root as menu_root, Item as MenuItem};
|
||||
use cosmic::{
|
||||
widget::menu::{ItemHeight, ItemWidth, MenuBar, Tree as MenuTree},
|
||||
Element,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{fl, Action, Config, Message};
|
||||
|
||||
pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> {
|
||||
let mut recent_items = Vec::new();
|
||||
|
||||
MenuBar::new(vec![MenuTree::with_children(
|
||||
menu_root(fl!("file")),
|
||||
menu_items(
|
||||
key_binds,
|
||||
vec![
|
||||
MenuItem::Button(fl!("open-media"), Action::FileOpen),
|
||||
MenuItem::Folder(fl!("open-recent-media"), recent_items),
|
||||
MenuItem::Button(fl!("close-file"), Action::FileClose),
|
||||
MenuItem::Divider,
|
||||
MenuItem::Button(fl!("quit"), Action::WindowClose),
|
||||
],
|
||||
),
|
||||
)])
|
||||
.item_height(ItemHeight::Dynamic(40))
|
||||
.item_width(ItemWidth::Uniform(240))
|
||||
.spacing(4.0)
|
||||
.into()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue