Add recent media, part of #53
This commit is contained in:
parent
fd1df4f098
commit
0169cccfa2
6 changed files with 144 additions and 9 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1070,6 +1070,7 @@ dependencies = [
|
||||||
name = "cosmic-player"
|
name = "cosmic-player"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"gstreamer-tag",
|
"gstreamer-tag",
|
||||||
"i18n-embed",
|
"i18n-embed",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
dirs = "5"
|
||||||
gstreamer-tag = "0.23"
|
gstreamer-tag = "0.23"
|
||||||
image = "0.24.9"
|
image = "0.24.9"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,6 @@ file = File
|
||||||
open-media = Open media...
|
open-media = Open media...
|
||||||
open-recent-media = Open recent media
|
open-recent-media = Open recent media
|
||||||
close-file = Close file
|
close-file = Close file
|
||||||
|
open-media-folder = Open media folder...
|
||||||
|
open-recent-media-folder = Open recent media folder
|
||||||
quit = Quit
|
quit = Quit
|
||||||
|
|
@ -5,6 +5,7 @@ use cosmic::{
|
||||||
theme,
|
theme,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
pub const CONFIG_VERSION: u64 = 1;
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
84
src/main.rs
84
src/main.rs
|
|
@ -30,7 +30,7 @@ use std::{
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, CONFIG_VERSION},
|
config::{Config, ConfigState, CONFIG_VERSION},
|
||||||
key_bind::{key_binds, KeyBind},
|
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();
|
let mut settings = Settings::default();
|
||||||
settings = settings.theme(config.app_theme.theme());
|
settings = settings.theme(config.app_theme.theme());
|
||||||
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
|
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 {
|
let flags = Flags {
|
||||||
config_handler,
|
config_handler,
|
||||||
config,
|
config,
|
||||||
|
config_state_handler,
|
||||||
|
config_state,
|
||||||
url_opt,
|
url_opt,
|
||||||
};
|
};
|
||||||
cosmic::app::run::<App>(settings, flags)?;
|
cosmic::app::run::<App>(settings, flags)?;
|
||||||
|
|
@ -123,6 +142,9 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
FileClose,
|
FileClose,
|
||||||
FileOpen,
|
FileOpen,
|
||||||
|
FileOpenRecent(usize),
|
||||||
|
FolderOpen,
|
||||||
|
FolderOpenRecent(usize),
|
||||||
Fullscreen,
|
Fullscreen,
|
||||||
PlayPause,
|
PlayPause,
|
||||||
SeekBackward,
|
SeekBackward,
|
||||||
|
|
@ -137,6 +159,9 @@ impl MenuAction for Action {
|
||||||
match self {
|
match self {
|
||||||
Self::FileClose => Message::FileClose,
|
Self::FileClose => Message::FileClose,
|
||||||
Self::FileOpen => Message::FileOpen,
|
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::Fullscreen => Message::Fullscreen,
|
||||||
Self::PlayPause => Message::PlayPause,
|
Self::PlayPause => Message::PlayPause,
|
||||||
Self::SeekBackward => Message::SeekRelative(-10.0),
|
Self::SeekBackward => Message::SeekRelative(-10.0),
|
||||||
|
|
@ -150,6 +175,8 @@ impl MenuAction for Action {
|
||||||
pub struct Flags {
|
pub struct Flags {
|
||||||
config_handler: Option<cosmic_config::Config>,
|
config_handler: Option<cosmic_config::Config>,
|
||||||
config: Config,
|
config: Config,
|
||||||
|
config_state_handler: Option<cosmic_config::Config>,
|
||||||
|
config_state: ConfigState,
|
||||||
url_opt: Option<url::Url>,
|
url_opt: Option<url::Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,10 +218,14 @@ pub enum MprisEvent {
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
None,
|
None,
|
||||||
Config(Config),
|
Config(Config),
|
||||||
|
ConfigState(ConfigState),
|
||||||
DropdownToggle(DropdownKind),
|
DropdownToggle(DropdownKind),
|
||||||
FileClose,
|
FileClose,
|
||||||
FileLoad(url::Url),
|
FileLoad(url::Url),
|
||||||
FileOpen,
|
FileOpen,
|
||||||
|
FileOpenRecent(usize),
|
||||||
|
FolderOpen,
|
||||||
|
FolderOpenRecent(usize),
|
||||||
Fullscreen,
|
Fullscreen,
|
||||||
Key(Modifiers, Key),
|
Key(Modifiers, Key),
|
||||||
AudioCode(usize),
|
AudioCode(usize),
|
||||||
|
|
@ -272,12 +303,18 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = match &self.flags.url_opt {
|
let url = match &self.flags.url_opt {
|
||||||
Some(some) => some,
|
Some(some) => some.clone(),
|
||||||
None => return Command::none(),
|
None => return Command::none(),
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("Loading {}", url);
|
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: 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.
|
//TODO: remove unwraps and enable playback of files with only audio.
|
||||||
let video = {
|
let video = {
|
||||||
|
|
@ -387,6 +424,14 @@ impl App {
|
||||||
self.update_title()
|
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) {
|
fn update_controls(&mut self, in_use: bool) {
|
||||||
if in_use
|
if in_use
|
||||||
|| !self
|
|| !self
|
||||||
|
|
@ -593,6 +638,12 @@ impl Application for App {
|
||||||
return self.update_config();
|
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) => {
|
Message::DropdownToggle(menu_kind) => {
|
||||||
if self.dropdown_opt.take() != Some(menu_kind) {
|
if self.dropdown_opt.take() != Some(menu_kind) {
|
||||||
self.dropdown_opt = Some(menu_kind);
|
self.dropdown_opt = Some(menu_kind);
|
||||||
|
|
@ -625,6 +676,15 @@ impl Application for App {
|
||||||
|x| x,
|
|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 => {
|
Message::Fullscreen => {
|
||||||
//TODO: cleanest way to close dropdowns
|
//TODO: cleanest way to close dropdowns
|
||||||
self.dropdown_opt = None;
|
self.dropdown_opt = None;
|
||||||
|
|
@ -818,7 +878,11 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
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.
|
/// Creates a view after each update.
|
||||||
|
|
@ -1094,6 +1158,7 @@ impl Application for App {
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<Self::Message> {
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
struct ConfigSubscription;
|
struct ConfigSubscription;
|
||||||
|
struct ConfigStateSubscription;
|
||||||
struct ThemeSubscription;
|
struct ThemeSubscription;
|
||||||
|
|
||||||
let mut subscriptions = vec![
|
let mut subscriptions = vec![
|
||||||
|
|
@ -1113,7 +1178,18 @@ impl Application for App {
|
||||||
if !update.errors.is_empty() {
|
if !update.errors.is_empty() {
|
||||||
log::debug!("errors loading config: {:?}", update.errors);
|
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>(
|
cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>(
|
||||||
TypeId::of::<ThemeSubscription>(),
|
TypeId::of::<ThemeSubscription>(),
|
||||||
|
|
|
||||||
49
src/menu.rs
49
src/menu.rs
|
|
@ -7,10 +7,43 @@ use cosmic::{
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
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> {
|
pub fn menu_bar<'a>(
|
||||||
let mut recent_items = Vec::new();
|
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(
|
MenuBar::new(vec![menu::Tree::with_children(
|
||||||
menu::root(fl!("file")),
|
menu::root(fl!("file")),
|
||||||
|
|
@ -18,15 +51,21 @@ pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> El
|
||||||
key_binds,
|
key_binds,
|
||||||
vec![
|
vec![
|
||||||
menu::Item::Button(fl!("open-media"), Action::FileOpen),
|
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::Button(fl!("close-file"), Action::FileClose),
|
||||||
menu::Item::Divider,
|
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),
|
menu::Item::Button(fl!("quit"), Action::WindowClose),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)])
|
)])
|
||||||
.item_height(ItemHeight::Dynamic(40))
|
.item_height(ItemHeight::Dynamic(40))
|
||||||
.item_width(ItemWidth::Uniform(240))
|
.item_width(ItemWidth::Uniform(320))
|
||||||
.spacing(theme::active().cosmic().spacing.space_xxxs.into())
|
.spacing(theme::active().cosmic().spacing.space_xxxs.into())
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue