// Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only use cosmic::app::{Core, Settings, Task}; use cosmic::command::set_theme; use cosmic::cosmic_config::{self, CosmicConfigEntry}; use cosmic::iced::event::{self, Event}; use cosmic::iced::keyboard::key::Physical; use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers}; use cosmic::iced::mouse::{Event as MouseEvent, ScrollDelta}; use cosmic::iced::window::{self, set_mode}; use cosmic::iced::{ Alignment, Background, Border, Color, ContentFit, Length, Limits, Subscription, }; use cosmic::widget::menu::action::MenuAction; use cosmic::widget::{self, Slider, nav_bar, segmented_button}; use cosmic::{Application, ApplicationExt, Element, action, cosmic_theme, executor, font, theme}; use iced_video_player::gst::prelude::*; use iced_video_player::{Video, VideoPlayer, gst, gst_pbutils}; use std::any::TypeId; use std::collections::HashMap; use std::ffi::{CStr, CString}; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use std::{fs, process, thread}; use tokio::sync::mpsc; use crate::config::{CONFIG_VERSION, Config, ConfigState, RepeatState}; use crate::key_bind::{KeyBind, key_binds}; use crate::project::ProjectNode; mod argparse; mod config; mod key_bind; mod localize; mod menu; #[cfg(feature = "mpris-server")] mod mpris; mod project; mod thumbnail; mod video; #[cfg(feature = "xdg-portal")] mod xdg_portals; static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0); const GST_PLAY_FLAG_VIDEO: i32 = 1 << 0; const GST_PLAY_FLAG_AUDIO: i32 = 1 << 1; const GST_PLAY_FLAG_TEXT: i32 = 1 << 2; const JUMP_BACKWARD_ICON: &[u8] = include_bytes!("../res/icons/hicolor/16x16/apps/jump-backward-10-symbolic.svg"); 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 { 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()) } fn get_framerate(video: &Video) -> Option { let pipeline = video.pipeline(); let video_sink: gst::Element = pipeline.property("video-sink"); let binding = video_sink.pads(); let pad = binding.first()?; let caps = pad.current_caps()?; let structure = caps.structure(0)?; let framerate = structure.get::("framerate").ok()?; Some(framerate.numer() as f64 / framerate.denom() as f64) } /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); let args = argparse::parse(); if let Some(output) = args.thumbnail_opt { let Some(input) = args.url_opt else { log::error!("thumbnailer can only handle exactly one URL"); process::exit(1); }; match thumbnail::main(&input, &output, args.size_opt) { Ok(()) => process::exit(0), Err(err) => { log::error!("failed to thumbnail '{}': {}", input, err); process::exit(1); } } } #[cfg(all(unix, not(target_os = "redox")))] match fork::daemon(true, true) { Ok(fork::Fork::Child) => (), Ok(fork::Fork::Parent(_child_pid)) => process::exit(0), Err(err) => { eprintln!("failed to daemonize: {:?}", err); process::exit(1); } } localize::localize(); let config = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) { Ok(config_handler) => { match Config::get_entry(&config_handler) { Ok(ok) => ok, Err((errs, config)) => { log::error!("errors loading config: {:?}", errs); config } } } Err(err) => { log::error!("failed to create config handler: {}", err); Config::default() } }; let (config_state_handler, config_state) = match cosmic_config::Config::new_state(App::APP_ID, CONFIG_VERSION) { Ok(config_state_handler) => { let config_state = ConfigState::get_entry(&config_state_handler).unwrap_or_else( |(errs, config_state)| { log::info!("errors loading config_state: {:?}", errs); config_state }, ); (Some(config_state_handler), config_state) } Err(err) => { log::error!("failed to create config_state handler: {}", err); (None, ConfigState::default()) } }; let mut settings = Settings::default(); settings = settings.theme(config.app_theme.theme()); settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0)); let flags = Flags { config, config_state_handler, config_state, url_opt: args.url_opt, urls: args.urls, }; cosmic::app::run::(settings, flags)?; Ok(()) } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Action { FileClose, FileOpen, FileClearRecents, FileOpenRecent(usize), FolderClose(usize), FolderOpen, FolderClearRecents, FolderOpenRecent(usize), Fullscreen, PlayPause, PlayPrev, PlayNext, SeekBackward, SeekForward, NextFrame, PreviousFrame, AbRepeat, WindowClose, } impl MenuAction for Action { type Message = Message; fn message(&self) -> Message { match self { Self::FileClose => Message::FileClose, Self::FileOpen => Message::FileOpen, Self::FileClearRecents => Message::FileClearRecents, Self::FileOpenRecent(index) => Message::FileOpenRecent(*index), Self::FolderClose(index) => Message::FolderClose(*index), Self::FolderOpen => Message::FolderOpen, Self::FolderClearRecents => Message::FolderClearRecents, Self::FolderOpenRecent(index) => Message::FolderOpenRecent(*index), Self::Fullscreen => Message::Fullscreen, Self::PlayPause => Message::PlayPause, Self::PlayPrev => Message::PlayPrev, Self::PlayNext => Message::PlayNext, Self::SeekBackward => Message::SeekRelative(-10.0), Self::SeekForward => Message::SeekRelative(10.0), Self::NextFrame => Message::NextFrame, Self::PreviousFrame => Message::PreviousFrame, Self::AbRepeat => Message::AbRepeat, Self::WindowClose => Message::WindowClose, } } } #[derive(Clone)] pub struct Flags { config: Config, config_state_handler: Option, config_state: ConfigState, url_opt: Option, urls: Option>, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DropdownKind { Audio, Speed, Subtitle, } #[derive(Clone, Debug, Default, PartialEq)] pub struct MprisMeta { url_opt: Option, album: String, album_art_opt: Option, album_artist: String, album_year_opt: Option, artists: Vec, title: String, disc_number: i32, track_number: i32, duration_micros: i64, } #[derive(Clone, Debug, Default, PartialEq)] pub struct MprisState { fullscreen: bool, position_micros: i64, paused: bool, volume: f64, repeat_state: RepeatState, } #[derive(Clone, Debug)] pub enum MprisEvent { Meta(MprisMeta), State(MprisState), } #[derive(Clone, Debug)] pub struct TextCode { pub id: Option, pub name: String, } impl AsRef for TextCode { fn as_ref(&self) -> &str { self.name.as_str() } } /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { Config(Config), ConfigState(ConfigState), DropdownToggle(DropdownKind), DurationChanged(Duration), FileClose, FileLoad(url::Url), FileOpen, FileClearRecents, FileOpenRecent(usize), FolderClose(usize), FolderLoad(PathBuf), FolderOpen, FolderClearRecents, FolderOpenRecent(usize), MultipleLoad(Vec), Fullscreen, Key(Modifiers, Physical, Key), AudioCode(usize), AudioToggle, AudioVolume(f64), TextCode(usize), Pause, Play, PlayPause, RepeatToggled(RepeatState), Scrolled(ScrollDelta), Seek(f64), SeekRelative(f64), SeekRelease, PlayPrev, PlayNext, NextFrame, PreviousFrame, EndOfStream, MissingPlugin(gst::Message), MprisChannel(MprisMeta, MprisState, mpsc::UnboundedSender), NewFrame, Reload, AbRepeat, VideoAreaClick, PlaybackSpeed(f64), ShowControls, SystemThemeModeChange(cosmic_theme::ThemeMode), WindowClose, } /// The [`App`] stores application-specific state. pub struct App { core: Core, flags: Flags, album_art_opt: Option, controls: bool, controls_time: Instant, dropdown_opt: Option, fullscreen: bool, key_binds: HashMap, mpris_meta: MprisMeta, mpris_opt: Option<(MprisMeta, MprisState, mpsc::UnboundedSender)>, nav_model: segmented_button::SingleSelectModel, projects: Vec<(String, PathBuf)>, video_opt: Option