// Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only use cosmic::{ app::{command, message, Command, Core, Settings}, cosmic_config::{self, CosmicConfigEntry}, cosmic_theme, executor, font, iced::{ event::{self, Event}, keyboard::{Event as KeyEvent, Key, Modifiers}, mouse::{Event as MouseEvent, ScrollDelta}, subscription::Subscription, window, Alignment, Background, Border, Color, ContentFit, Length, Limits, }, iced_style, theme, widget::{self, menu::action::MenuAction, nav_bar, segmented_button, Slider}, Application, ApplicationExt, Element, }; use iced_video_player::{ gst::{self, prelude::*}, gst_app, gst_pbutils, Video, VideoPlayer, }; use std::{ any::TypeId, collections::HashMap, ffi::{CStr, CString}, fs, path::{Path, PathBuf}, process, thread, time::{Duration, Instant}, }; use tokio::sync::mpsc; use crate::{ config::{Config, ConfigState, CONFIG_VERSION}, key_bind::{key_binds, KeyBind}, project::ProjectNode, }; mod argparse; mod config; mod key_bind; mod localize; mod menu; #[cfg(feature = "mpris-server")] mod mpris; mod project; 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; 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()) } /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { #[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); } } env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); localize::localize(); let args = argparse::parse(); let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) { Ok(config_handler) => { let config = match Config::get_entry(&config_handler) { Ok(ok) => ok, Err((errs, config)) => { log::error!("errors loading config: {:?}", errs); config } }; (Some(config_handler), config) } Err(err) => { log::error!("failed to create config handler: {}", err); (None, 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_handler, 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, FileOpenRecent(usize), FolderClose(usize), FolderOpen, FolderOpenRecent(usize), Fullscreen, PlayPause, SeekBackward, SeekForward, WindowClose, } impl MenuAction for Action { type Message = Message; fn message(&self) -> Message { match self { Self::FileClose => Message::FileClose, Self::FileOpen => Message::FileOpen, Self::FileOpenRecent(index) => Message::FileOpenRecent(*index), Self::FolderClose(index) => Message::FolderClose(*index), Self::FolderOpen => Message::FolderOpen, Self::FolderOpenRecent(index) => Message::FolderOpenRecent(*index), Self::Fullscreen => Message::Fullscreen, Self::PlayPause => Message::PlayPause, Self::SeekBackward => Message::SeekRelative(-10.0), Self::SeekForward => Message::SeekRelative(10.0), Self::WindowClose => Message::WindowClose, } } } #[derive(Clone)] pub struct Flags { config_handler: Option, config: Config, config_state_handler: Option, config_state: ConfigState, url_opt: Option, urls: Option>, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DropdownKind { Audio, 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, } #[derive(Clone, Debug)] pub enum MprisEvent { Meta(MprisMeta), State(MprisState), } /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { None, Config(Config), ConfigState(ConfigState), DropdownToggle(DropdownKind), FileClose, FileLoad(url::Url), FileOpen, FileOpenRecent(usize), FolderClose(usize), FolderLoad(PathBuf), FolderOpen, FolderOpenRecent(usize), MultipleLoad(Vec), Fullscreen, Key(Modifiers, Key), AudioCode(usize), AudioToggle, AudioVolume(f64), TextCode(usize), Pause, Play, PlayPause, Scrolled(ScrollDelta), Seek(f64), SeekRelative(f64), SeekRelease, EndOfStream, MissingPlugin(gst::Message), MprisChannel(MprisMeta, MprisState, mpsc::UnboundedSender), NewFrame, Reload, 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