From 0169cccfa2b65b5f7e57c240a04dfd9db17c4221 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 24 Jan 2025 12:49:02 -0700 Subject: [PATCH] Add recent media, part of #53 --- Cargo.lock | 1 + Cargo.toml | 1 + i18n/en/cosmic_player.ftl | 2 + src/config.rs | 16 ++++++++ src/main.rs | 84 +++++++++++++++++++++++++++++++++++++-- src/menu.rs | 49 ++++++++++++++++++++--- 6 files changed, 144 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc2301e..72eca37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1070,6 +1070,7 @@ dependencies = [ name = "cosmic-player" version = "0.1.0" dependencies = [ + "dirs", "env_logger", "gstreamer-tag", "i18n-embed", diff --git a/Cargo.toml b/Cargo.toml index a182ca6..7b89bcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +dirs = "5" gstreamer-tag = "0.23" image = "0.24.9" lazy_static = "1" diff --git a/i18n/en/cosmic_player.ftl b/i18n/en/cosmic_player.ftl index d6a0c2a..932ec08 100644 --- a/i18n/en/cosmic_player.ftl +++ b/i18n/en/cosmic_player.ftl @@ -22,4 +22,6 @@ file = File open-media = Open media... open-recent-media = Open recent media close-file = Close file +open-media-folder = Open media folder... +open-recent-media-folder = Open recent media folder quit = Quit \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 362dd6b..233cf48 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use cosmic::{ theme, }; use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; pub const CONFIG_VERSION: u64 = 1; @@ -38,3 +39,18 @@ impl Default for Config { } } } + +#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ConfigState { + pub recent_files: VecDeque, + pub recent_folders: VecDeque, +} + +impl Default for ConfigState { + fn default() -> Self { + Self { + recent_files: VecDeque::new(), + recent_folders: VecDeque::new(), + } + } +} diff --git a/src/main.rs b/src/main.rs index 94ec49c..6a66dff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,7 @@ use std::{ use tokio::sync::mpsc; use crate::{ - config::{Config, CONFIG_VERSION}, + config::{Config, ConfigState, CONFIG_VERSION}, key_bind::{key_binds, KeyBind}, }; @@ -85,6 +85,23 @@ pub fn main() -> Result<(), Box> { } }; + 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)); @@ -112,6 +129,8 @@ pub fn main() -> Result<(), Box> { let flags = Flags { config_handler, config, + config_state_handler, + config_state, url_opt, }; cosmic::app::run::(settings, flags)?; @@ -123,6 +142,9 @@ pub fn main() -> Result<(), Box> { pub enum Action { FileClose, FileOpen, + FileOpenRecent(usize), + FolderOpen, + FolderOpenRecent(usize), Fullscreen, PlayPause, SeekBackward, @@ -137,6 +159,9 @@ impl MenuAction for Action { match self { Self::FileClose => Message::FileClose, Self::FileOpen => Message::FileOpen, + Self::FileOpenRecent(index) => Message::FileOpenRecent(*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), @@ -150,6 +175,8 @@ impl MenuAction for Action { pub struct Flags { config_handler: Option, config: Config, + config_state_handler: Option, + config_state: ConfigState, url_opt: Option, } @@ -191,10 +218,14 @@ pub enum MprisEvent { pub enum Message { None, Config(Config), + ConfigState(ConfigState), DropdownToggle(DropdownKind), FileClose, FileLoad(url::Url), FileOpen, + FileOpenRecent(usize), + FolderOpen, + FolderOpenRecent(usize), Fullscreen, Key(Modifiers, Key), AudioCode(usize), @@ -272,12 +303,18 @@ impl App { } let url = match &self.flags.url_opt { - Some(some) => some, + Some(some) => some.clone(), None => return Command::none(), }; log::info!("Loading {}", url); + // Add to recent files, ensuring only one entry + self.flags.config_state.recent_files.retain(|x| x != &url); + self.flags.config_state.recent_files.push_front(url.clone()); + self.flags.config_state.recent_files.truncate(10); + self.save_config_state(); + //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 = { @@ -387,6 +424,14 @@ impl App { self.update_title() } + fn save_config_state(&mut self) { + if let Some(ref config_state_handler) = self.flags.config_state_handler { + if let Err(err) = self.flags.config_state.write_entry(config_state_handler) { + log::error!("failed to save config_state: {}", err); + } + } + } + fn update_controls(&mut self, in_use: bool) { if in_use || !self @@ -593,6 +638,12 @@ impl Application for App { return self.update_config(); } } + Message::ConfigState(config_state) => { + if config_state != self.flags.config_state { + log::info!("update config state"); + self.flags.config_state = config_state; + } + } Message::DropdownToggle(menu_kind) => { if self.dropdown_opt.take() != Some(menu_kind) { self.dropdown_opt = Some(menu_kind); @@ -625,6 +676,15 @@ impl Application for App { |x| x, ); } + Message::FileOpenRecent(index) => { + if let Some(url) = self.flags.config_state.recent_files.get(index) { + self.flags.url_opt = Some(url.clone()); + return self.load(); + } + } + Message::FolderOpen | Message::FolderOpenRecent(..) => { + log::error!("TODO: {:?}", message); + } Message::Fullscreen => { //TODO: cleanest way to close dropdowns self.dropdown_opt = None; @@ -818,7 +878,11 @@ impl Application for App { } fn header_start(&self) -> Vec> { - vec![menu::menu_bar(&self.flags.config, &self.key_binds)] + vec![menu::menu_bar( + &self.flags.config, + &self.flags.config_state, + &self.key_binds, + )] } /// Creates a view after each update. @@ -1094,6 +1158,7 @@ impl Application for App { fn subscription(&self) -> Subscription { struct ConfigSubscription; + struct ConfigStateSubscription; struct ThemeSubscription; let mut subscriptions = vec![ @@ -1113,7 +1178,18 @@ impl Application for App { if !update.errors.is_empty() { log::debug!("errors loading config: {:?}", update.errors); } - Message::SystemThemeModeChange(update.config) + Message::Config(update.config) + }), + cosmic_config::config_state_subscription( + TypeId::of::(), + Self::APP_ID.into(), + CONFIG_VERSION, + ) + .map(|update| { + if !update.errors.is_empty() { + log::debug!("errors loading config state: {:?}", update.errors); + } + Message::ConfigState(update.config) }), cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>( TypeId::of::(), diff --git a/src/menu.rs b/src/menu.rs index aed40eb..116716c 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -7,10 +7,43 @@ use cosmic::{ }; use std::collections::HashMap; -use crate::{fl, Action, Config, Message}; +use crate::{fl, Action, Config, ConfigState, Message}; -pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap) -> Element<'a, Message> { - let mut recent_items = Vec::new(); +pub fn menu_bar<'a>( + config: &Config, + config_state: &ConfigState, + key_binds: &HashMap, +) -> Element<'a, Message> { + let home_dir_opt = dirs::home_dir(); + let format_path = |url: &url::Url| -> String { + match url.to_file_path() { + Ok(path) => { + if let Some(home_dir) = &home_dir_opt { + if let Ok(part) = path.strip_prefix(home_dir) { + return format!("~/{}", part.display()); + } + } + path.display().to_string() + } + Err(()) => url.to_string(), + } + }; + + let mut recent_files = Vec::with_capacity(config_state.recent_files.len()); + for (i, path) in config_state.recent_files.iter().enumerate() { + recent_files.push(menu::Item::Button( + format_path(path), + Action::FileOpenRecent(i), + )); + } + + let mut recent_folders = Vec::with_capacity(config_state.recent_folders.len()); + for (i, path) in config_state.recent_folders.iter().enumerate() { + recent_folders.push(menu::Item::Button( + format_path(path), + Action::FolderOpenRecent(i), + )); + } MenuBar::new(vec![menu::Tree::with_children( menu::root(fl!("file")), @@ -18,15 +51,21 @@ pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap) -> El key_binds, vec![ menu::Item::Button(fl!("open-media"), Action::FileOpen), - menu::Item::Folder(fl!("open-recent-media"), recent_items), + menu::Item::Folder(fl!("open-recent-media"), recent_files), menu::Item::Button(fl!("close-file"), Action::FileClose), menu::Item::Divider, + /*TODO: folders + menu::Item::Button(fl!("open-media-folder"), Action::FolderOpen), + menu::Item::Folder(fl!("open-recent-media-folder"), recent_folders), + menu::Item::Folder(fl!("close-media-folder"), close_folders), + menu::Item::Divider, + */ menu::Item::Button(fl!("quit"), Action::WindowClose), ], ), )]) .item_height(ItemHeight::Dynamic(40)) - .item_width(ItemWidth::Uniform(240)) + .item_width(ItemWidth::Uniform(320)) .spacing(theme::active().cosmic().spacing.space_xxxs.into()) .into() }