Add recent media, part of #53

This commit is contained in:
Jeremy Soller 2025-01-24 12:49:02 -07:00
parent fd1df4f098
commit 0169cccfa2
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
6 changed files with 144 additions and 9 deletions

View file

@ -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<url::Url>,
pub recent_folders: VecDeque<url::Url>,
}
impl Default for ConfigState {
fn default() -> Self {
Self {
recent_files: VecDeque::new(),
recent_folders: VecDeque::new(),
}
}
}

View file

@ -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<dyn std::error::Error>> {
}
};
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<dyn std::error::Error>> {
let flags = Flags {
config_handler,
config,
config_state_handler,
config_state,
url_opt,
};
cosmic::app::run::<App>(settings, flags)?;
@ -123,6 +142,9 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
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<cosmic_config::Config>,
config: Config,
config_state_handler: Option<cosmic_config::Config>,
config_state: ConfigState,
url_opt: Option<url::Url>,
}
@ -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<Element<Self::Message>> {
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<Self::Message> {
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::<ConfigStateSubscription>(),
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::<ThemeSubscription>(),

View file

@ -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<KeyBind, Action>) -> Element<'a, Message> {
let mut recent_items = Vec::new();
pub fn menu_bar<'a>(
config: &Config,
config_state: &ConfigState,
key_binds: &HashMap<KeyBind, Action>,
) -> 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<KeyBind, Action>) -> 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()
}