parent
0169cccfa2
commit
841816e8d1
8 changed files with 557 additions and 59 deletions
47
Cargo.lock
generated
47
Cargo.lock
generated
|
|
@ -1076,6 +1076,8 @@ dependencies = [
|
|||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
"iced_video_player",
|
||||
"icu_collator",
|
||||
"icu_provider",
|
||||
"image",
|
||||
"lazy_static",
|
||||
"libcosmic",
|
||||
|
|
@ -2484,9 +2486,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "i18n-embed"
|
||||
version = "0.13.9"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92a86226a7a16632de6723449ee5fe70bac5af718bc642ee9ca2f0f6e14fa1fa"
|
||||
checksum = "94205d95764f5bb9db9ea98fa77f89653365ca748e27161f5bbea2ffd50e459c"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"fluent",
|
||||
|
|
@ -2506,9 +2508,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "i18n-embed-fl"
|
||||
version = "0.6.7"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d26a3d3569737dfaac7fc1c4078e6af07471c3060b8e570bcd83cdd5f4685395"
|
||||
checksum = "9fc1f8715195dffc4caddcf1cf3128da15fe5d8a137606ea8856c9300047d5a2"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"find-crate",
|
||||
|
|
@ -2797,6 +2799,31 @@ dependencies = [
|
|||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collator"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d370371887d31d56f361c3eaa15743e54f13bc677059c9191c77e099ed6966b2"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collator_data",
|
||||
"icu_collections",
|
||||
"icu_locid_transform",
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"utf8_iter",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collator_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ee3f88741364b7d6269cce6827a3e6a8a2cf408a78f766c9224ab479d5e4ae5"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "1.5.0"
|
||||
|
|
@ -4442,9 +4469,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
|||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "6.8.1"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
|
||||
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
|
|
@ -4453,9 +4480,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "6.8.1"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
|
||||
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -4466,9 +4493,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "7.8.1"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
|
||||
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ tempfile = "3"
|
|||
tokio = "1"
|
||||
url = "2"
|
||||
# Internationalization
|
||||
i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }
|
||||
i18n-embed-fl = "0.6"
|
||||
rust-embed = "6"
|
||||
icu_collator = "1.5"
|
||||
icu_provider = { version = "1.5", features = ["sync"] }
|
||||
i18n-embed = { version = "0.14", features = ["fluent-system", "desktop-requester"] }
|
||||
i18n-embed-fl = "0.7"
|
||||
rust-embed = "8"
|
||||
# Logging
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
audio = Audio
|
||||
no-video-or-audio-file-open = No video or audio file open
|
||||
open-file = Open file
|
||||
open-folder = Open folder
|
||||
subtitles = Subtitles
|
||||
|
||||
# Context Pages
|
||||
|
|
@ -24,4 +25,5 @@ open-recent-media = Open recent media
|
|||
close-file = Close file
|
||||
open-media-folder = Open media folder...
|
||||
open-recent-media-folder = Open recent media folder
|
||||
close-media-folder = Close media folder
|
||||
quit = Quit
|
||||
|
|
@ -5,7 +5,7 @@ use cosmic::{
|
|||
theme,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::{collections::VecDeque, path::PathBuf};
|
||||
|
||||
pub const CONFIG_VERSION: u64 = 1;
|
||||
|
||||
|
|
@ -43,14 +43,14 @@ 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>,
|
||||
pub recent_projects: VecDeque<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for ConfigState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
recent_files: VecDeque::new(),
|
||||
recent_folders: VecDeque::new(),
|
||||
recent_projects: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,37 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use i18n_embed::{
|
||||
fluent::{fluent_language_loader, FluentLanguageLoader},
|
||||
DefaultLocalizer, LanguageLoader, Localizer,
|
||||
};
|
||||
use icu_collator::{Collator, CollatorOptions, Numeric};
|
||||
use icu_provider::DataLocale;
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "i18n/"]
|
||||
struct Localizations;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref LANGUAGE_LOADER: FluentLanguageLoader = {
|
||||
pub static LANGUAGE_LOADER: OnceLock<FluentLanguageLoader> = OnceLock::new();
|
||||
pub static LANGUAGE_SORTER: OnceLock<Collator> = OnceLock::new();
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fl {
|
||||
($message_id:literal) => {{
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER.get().unwrap(), $message_id)
|
||||
}};
|
||||
|
||||
($message_id:literal, $($args:expr),*) => {{
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER.get().unwrap(), $message_id, $($args), *)
|
||||
}};
|
||||
}
|
||||
|
||||
// Get the `Localizer` to be used for localizing this library.
|
||||
pub fn localizer() -> Box<dyn Localizer> {
|
||||
LANGUAGE_LOADER.get_or_init(|| {
|
||||
let loader: FluentLanguageLoader = fluent_language_loader!();
|
||||
|
||||
loader
|
||||
|
|
@ -19,23 +39,12 @@ lazy_static::lazy_static! {
|
|||
.expect("Error while loading fallback language");
|
||||
|
||||
loader
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fl {
|
||||
($message_id:literal) => {{
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id)
|
||||
}};
|
||||
|
||||
($message_id:literal, $($args:expr),*) => {{
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *)
|
||||
}};
|
||||
}
|
||||
|
||||
// Get the `Localizer` to be used for localizing this library.
|
||||
pub fn localizer() -> Box<dyn Localizer> {
|
||||
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
|
||||
Box::from(DefaultLocalizer::new(
|
||||
LANGUAGE_LOADER.get().unwrap(),
|
||||
&Localizations,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn localize() {
|
||||
|
|
@ -46,3 +55,22 @@ pub fn localize() {
|
|||
eprintln!("Error while loading language for App List {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sorter() -> &'static Collator {
|
||||
LANGUAGE_SORTER.get_or_init(|| {
|
||||
let mut options = CollatorOptions::new();
|
||||
options.numeric = Some(Numeric::On);
|
||||
let localizer = localizer();
|
||||
let language_loader = localizer.language_loader();
|
||||
|
||||
DataLocale::from_str(&language_loader.current_language().to_string())
|
||||
.or_else(|_| DataLocale::from_str(&language_loader.fallback_language().to_string()))
|
||||
.ok()
|
||||
.and_then(|locale| Collator::try_new(&locale, options).ok())
|
||||
.or_else(|| {
|
||||
let locale = DataLocale::from_str("en-US").expect("en-US is a valid BCP-47 tag");
|
||||
Collator::try_new(&locale, options).ok()
|
||||
})
|
||||
.expect("Creating a collator from the system's current language, the fallback language, or American English should succeed")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
346
src/main.rs
346
src/main.rs
|
|
@ -12,8 +12,8 @@ use cosmic::{
|
|||
subscription::Subscription,
|
||||
window, Alignment, Background, Border, Color, ContentFit, Length, Limits,
|
||||
},
|
||||
theme,
|
||||
widget::{self, menu::action::MenuAction, Slider},
|
||||
iced_style, theme,
|
||||
widget::{self, menu::action::MenuAction, nav_bar, segmented_button, Slider},
|
||||
Application, ApplicationExt, Element,
|
||||
};
|
||||
use iced_video_player::{
|
||||
|
|
@ -24,7 +24,9 @@ use std::{
|
|||
any::TypeId,
|
||||
collections::HashMap,
|
||||
ffi::{CStr, CString},
|
||||
fs, process, thread,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process, thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
|
@ -32,6 +34,7 @@ use tokio::sync::mpsc;
|
|||
use crate::{
|
||||
config::{Config, ConfigState, CONFIG_VERSION},
|
||||
key_bind::{key_binds, KeyBind},
|
||||
project::ProjectNode,
|
||||
};
|
||||
|
||||
mod config;
|
||||
|
|
@ -40,6 +43,7 @@ mod localize;
|
|||
mod menu;
|
||||
#[cfg(feature = "mpris-server")]
|
||||
mod mpris;
|
||||
mod project;
|
||||
|
||||
static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0);
|
||||
|
||||
|
|
@ -143,6 +147,7 @@ pub enum Action {
|
|||
FileClose,
|
||||
FileOpen,
|
||||
FileOpenRecent(usize),
|
||||
FolderClose(usize),
|
||||
FolderOpen,
|
||||
FolderOpenRecent(usize),
|
||||
Fullscreen,
|
||||
|
|
@ -160,6 +165,7 @@ impl MenuAction for Action {
|
|||
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,
|
||||
|
|
@ -224,6 +230,8 @@ pub enum Message {
|
|||
FileLoad(url::Url),
|
||||
FileOpen,
|
||||
FileOpenRecent(usize),
|
||||
FolderClose(usize),
|
||||
FolderLoad(PathBuf),
|
||||
FolderOpen,
|
||||
FolderOpenRecent(usize),
|
||||
Fullscreen,
|
||||
|
|
@ -259,6 +267,8 @@ pub struct App {
|
|||
fullscreen: bool,
|
||||
key_binds: HashMap<KeyBind, Action>,
|
||||
mpris_opt: Option<(MprisMeta, MprisState, mpsc::UnboundedSender<MprisEvent>)>,
|
||||
nav_model: segmented_button::SingleSelectModel,
|
||||
projects: Vec<(String, PathBuf)>,
|
||||
video_opt: Option<Video>,
|
||||
position: f64,
|
||||
duration: f64,
|
||||
|
|
@ -293,6 +303,7 @@ impl App {
|
|||
self.text_codes.clear();
|
||||
self.current_text = -1;
|
||||
self.update_mpris_meta();
|
||||
self.update_nav_bar_active();
|
||||
was_open
|
||||
}
|
||||
|
||||
|
|
@ -424,6 +435,128 @@ impl App {
|
|||
self.update_title()
|
||||
}
|
||||
|
||||
fn open_folder<P: AsRef<Path>>(&mut self, path: P, mut position: u16, indent: u16) {
|
||||
let read_dir = match fs::read_dir(&path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::error!("failed to read directory {:?}: {}", path.as_ref(), err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
for entry_res in read_dir {
|
||||
let entry = match entry_res {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"failed to read entry in directory {:?}: {}",
|
||||
path.as_ref(),
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let entry_path = entry.path();
|
||||
let node = match ProjectNode::new(&entry_path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"failed to open directory {:?} entry {:?}: {}",
|
||||
path.as_ref(),
|
||||
entry_path,
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
nodes.sort();
|
||||
|
||||
for node in nodes {
|
||||
let mut entity = self
|
||||
.nav_model
|
||||
.insert()
|
||||
.position(position)
|
||||
.indent(indent)
|
||||
.text(node.name().to_string());
|
||||
if let Some(icon) = node.icon(16) {
|
||||
entity = entity.icon(icon);
|
||||
}
|
||||
entity.data(node);
|
||||
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_project<P: AsRef<Path>>(&mut self, path: P) {
|
||||
let path = path.as_ref();
|
||||
let node = match ProjectNode::new(path) {
|
||||
Ok(mut node) => {
|
||||
match &mut node {
|
||||
ProjectNode::Folder {
|
||||
name,
|
||||
path,
|
||||
open,
|
||||
root,
|
||||
} => {
|
||||
*open = true;
|
||||
*root = true;
|
||||
|
||||
for (_project_name, project_path) in self.projects.iter() {
|
||||
if project_path == path {
|
||||
// Project already open
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the absolute path
|
||||
self.projects.push((name.to_string(), path.to_path_buf()));
|
||||
|
||||
// Add to recent projects, ensuring only one entry
|
||||
self.flags
|
||||
.config_state
|
||||
.recent_projects
|
||||
.retain(|x| x != path);
|
||||
self.flags
|
||||
.config_state
|
||||
.recent_projects
|
||||
.push_front(path.to_path_buf());
|
||||
self.flags.config_state.recent_projects.truncate(10);
|
||||
self.save_config_state();
|
||||
|
||||
// Open nav bar
|
||||
self.core.nav_bar_set_toggled(true);
|
||||
}
|
||||
_ => {
|
||||
log::error!("failed to open project {:?}: not a directory", path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
node
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to open project {:?}: {}", path, err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut entity = self.nav_model.insert().text(node.name().to_string());
|
||||
if let Some(icon) = node.icon(16) {
|
||||
entity = entity.icon(icon);
|
||||
}
|
||||
entity = entity.data(node);
|
||||
|
||||
let id = entity.id();
|
||||
|
||||
let position = self.nav_model.position(id).unwrap_or(0);
|
||||
|
||||
self.open_folder(path, position + 1, 1);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -561,6 +694,52 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_nav_bar_active(&mut self) {
|
||||
let tab_path_opt = match &self.flags.url_opt {
|
||||
Some(url) => url.to_file_path().ok(),
|
||||
None => None,
|
||||
};
|
||||
|
||||
// Locate tree node to activate
|
||||
let mut active_id = segmented_button::Entity::default();
|
||||
|
||||
if let Some(tab_path) = tab_path_opt {
|
||||
// Automatically expand tree to find and select active file
|
||||
loop {
|
||||
let mut expand_opt = None;
|
||||
for id in self.nav_model.iter() {
|
||||
if let Some(node) = self.nav_model.data(id) {
|
||||
match node {
|
||||
ProjectNode::Folder { path, open, .. } => {
|
||||
if tab_path.starts_with(path) && !*open {
|
||||
expand_opt = Some(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ProjectNode::File { path, .. } => {
|
||||
if path == &tab_path {
|
||||
active_id = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match expand_opt {
|
||||
Some(id) => {
|
||||
//TODO: can this be optimized?
|
||||
// Task not used becuase opening a folder just returns Task::none
|
||||
let _ = self.on_nav_select(id);
|
||||
}
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.nav_model.activate(active_id);
|
||||
}
|
||||
|
||||
fn update_title(&mut self) -> Command<Message> {
|
||||
//TODO: filename?
|
||||
let title = "COSMIC Media Player";
|
||||
|
|
@ -604,6 +783,8 @@ impl Application for App {
|
|||
fullscreen: false,
|
||||
key_binds: key_binds(),
|
||||
mpris_opt: None,
|
||||
nav_model: nav_bar::Model::builder().build(),
|
||||
projects: Vec::new(),
|
||||
video_opt: None,
|
||||
position: 0.0,
|
||||
duration: 0.0,
|
||||
|
|
@ -615,10 +796,25 @@ impl Application for App {
|
|||
current_text: -1,
|
||||
};
|
||||
|
||||
// Do not show nav bar by default. Will be opened by open_project if needed
|
||||
app.core.nav_bar_set_toggled(false);
|
||||
//TODO: handle command line arguments that are folders?
|
||||
|
||||
// Add button to open a project
|
||||
//TODO: remove and show this based on open projects?
|
||||
app.nav_model
|
||||
.insert()
|
||||
.icon(widget::icon::from_name("folder-open-symbolic").size(16))
|
||||
.text(fl!("open-folder"));
|
||||
|
||||
let command = app.load();
|
||||
(app, command)
|
||||
}
|
||||
|
||||
fn nav_model(&self) -> Option<&nav_bar::Model> {
|
||||
Some(&self.nav_model)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> Command<Self::Message> {
|
||||
if self.fullscreen {
|
||||
return self.update(Message::Fullscreen);
|
||||
|
|
@ -627,6 +823,78 @@ impl Application for App {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Message> {
|
||||
// Toggle open state and get clone of node data
|
||||
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
|
||||
Some(node) => {
|
||||
if let ProjectNode::Folder { open, .. } = node {
|
||||
*open = !*open;
|
||||
}
|
||||
Some(node.clone())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
match node_opt {
|
||||
Some(node) => {
|
||||
// Update icon
|
||||
if let Some(icon) = node.icon(16) {
|
||||
self.nav_model.icon_set(id, icon);
|
||||
} else {
|
||||
self.nav_model.icon_remove(id);
|
||||
}
|
||||
|
||||
match node {
|
||||
ProjectNode::Folder { path, open, .. } => {
|
||||
let position = self.nav_model.position(id).unwrap_or(0);
|
||||
let indent = self.nav_model.indent(id).unwrap_or(0);
|
||||
if open {
|
||||
// Open folder
|
||||
self.open_folder(path, position + 1, indent + 1);
|
||||
} else {
|
||||
// Close folder
|
||||
while let Some(child_id) = self.nav_model.entity_at(position + 1) {
|
||||
if self.nav_model.indent(child_id).unwrap_or(0) > indent {
|
||||
self.nav_model.remove(child_id);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent nav bar from closing when selecting a
|
||||
// folder in condensed mode.
|
||||
self.core_mut().nav_bar_set_toggled(true);
|
||||
|
||||
Command::none()
|
||||
}
|
||||
ProjectNode::File { path, .. } => match url::Url::from_file_path(&path) {
|
||||
Ok(url) => self.update(Message::FileLoad(url)),
|
||||
Err(()) => {
|
||||
log::warn!("failed to convert {:?} to url", path);
|
||||
Command::none()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Open folder
|
||||
self.update(Message::FolderOpen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<theme::Application> {
|
||||
// This ensures we have a solid background color even when using no content container
|
||||
Some(theme::Application::Custom(Box::new(|theme| {
|
||||
iced_style::application::Appearance {
|
||||
background_color: theme.cosmic().bg_color().into(),
|
||||
icon_color: theme.cosmic().on_bg_color().into(),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Handle application events here.
|
||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||
match message {
|
||||
|
|
@ -678,12 +946,75 @@ impl Application for App {
|
|||
}
|
||||
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();
|
||||
return self.update(Message::FileLoad(url.clone()));
|
||||
}
|
||||
}
|
||||
Message::FolderOpen | Message::FolderOpenRecent(..) => {
|
||||
log::error!("TODO: {:?}", message);
|
||||
Message::FolderClose(project_i) => {
|
||||
if project_i < self.projects.len() {
|
||||
let (_project_name, project_path) = self.projects.remove(project_i);
|
||||
let mut position = 0;
|
||||
let mut closing = false;
|
||||
while let Some(id) = self.nav_model.entity_at(position) {
|
||||
match self.nav_model.data::<ProjectNode>(id) {
|
||||
Some(node) => {
|
||||
if let ProjectNode::Folder { path, root, .. } = node {
|
||||
if path == &project_path {
|
||||
// Found the project root node, closing
|
||||
closing = true;
|
||||
} else if *root && closing {
|
||||
// Found another project root node after closing, breaking
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if closing {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if closing {
|
||||
self.nav_model.remove(id);
|
||||
} else {
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::FolderLoad(path) => {
|
||||
self.open_project(path);
|
||||
}
|
||||
Message::FolderOpen => {
|
||||
//TODO: embed cosmic-files dialog (after libcosmic rebase works)
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
return Command::perform(
|
||||
async move {
|
||||
let dialog = cosmic::dialog::file_chooser::open::Dialog::new()
|
||||
.title(fl!("open-media-folder"));
|
||||
match dialog.open_folder().await {
|
||||
Ok(response) => {
|
||||
let url = response.url();
|
||||
match url.to_file_path() {
|
||||
Ok(path) => message::app(Message::FolderLoad(path)),
|
||||
Err(()) => {
|
||||
log::warn!("unsupported folder URL {:?}", url);
|
||||
message::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to open folder: {}", err);
|
||||
message::none()
|
||||
}
|
||||
}
|
||||
},
|
||||
|x| x,
|
||||
);
|
||||
}
|
||||
Message::FolderOpenRecent(index) => {
|
||||
if let Some(path) = self.flags.config_state.recent_projects.get(index) {
|
||||
return self.update(Message::FolderLoad(path.clone()));
|
||||
}
|
||||
}
|
||||
Message::Fullscreen => {
|
||||
//TODO: cleanest way to close dropdowns
|
||||
|
|
@ -882,6 +1213,7 @@ impl Application for App {
|
|||
&self.flags.config,
|
||||
&self.flags.config_state,
|
||||
&self.key_binds,
|
||||
&self.projects,
|
||||
)]
|
||||
}
|
||||
|
||||
|
|
|
|||
44
src/menu.rs
44
src/menu.rs
|
|
@ -5,7 +5,7 @@ use cosmic::{
|
|||
widget::menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar},
|
||||
Element,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::{fl, Action, Config, ConfigState, Message};
|
||||
|
||||
|
|
@ -13,18 +13,20 @@ pub fn menu_bar<'a>(
|
|||
config: &Config,
|
||||
config_state: &ConfigState,
|
||||
key_binds: &HashMap<KeyBind, Action>,
|
||||
projects: &Vec<(String, PathBuf)>,
|
||||
) -> 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()
|
||||
let format_path = |path: &PathBuf| -> String {
|
||||
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()
|
||||
};
|
||||
let format_url = |url: &url::Url| -> String {
|
||||
match url.to_file_path() {
|
||||
Ok(path) => format_path(&path),
|
||||
Err(()) => url.to_string(),
|
||||
}
|
||||
};
|
||||
|
|
@ -32,19 +34,27 @@ pub fn menu_bar<'a>(
|
|||
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),
|
||||
format_url(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(
|
||||
let mut recent_projects = Vec::with_capacity(config_state.recent_projects.len());
|
||||
for (i, path) in config_state.recent_projects.iter().enumerate() {
|
||||
recent_projects.push(menu::Item::Button(
|
||||
format_path(path),
|
||||
Action::FolderOpenRecent(i),
|
||||
));
|
||||
}
|
||||
|
||||
let mut close_projects = Vec::with_capacity(projects.len());
|
||||
for (folder_i, (name, _path)) in projects.iter().enumerate() {
|
||||
close_projects.push(menu::Item::Button(
|
||||
name.clone(),
|
||||
Action::FolderClose(folder_i),
|
||||
));
|
||||
}
|
||||
|
||||
MenuBar::new(vec![menu::Tree::with_children(
|
||||
menu::root(fl!("file")),
|
||||
menu::items(
|
||||
|
|
@ -54,12 +64,10 @@ pub fn menu_bar<'a>(
|
|||
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::Folder(fl!("open-recent-media-folder"), recent_projects),
|
||||
menu::Item::Folder(fl!("close-media-folder"), close_projects),
|
||||
menu::Item::Divider,
|
||||
*/
|
||||
menu::Item::Button(fl!("quit"), Action::WindowClose),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
99
src/project.rs
Normal file
99
src/project.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::widget;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ProjectNode {
|
||||
Folder {
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
open: bool,
|
||||
root: bool,
|
||||
},
|
||||
File {
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProjectNode {
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
||||
let path = fs::canonicalize(path)?;
|
||||
let name = path
|
||||
.file_name()
|
||||
.ok_or(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("path {:?} has no file name", path),
|
||||
))?
|
||||
.to_str()
|
||||
.ok_or(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("path {:?} is not valid UTF-8", path),
|
||||
))?
|
||||
.to_string();
|
||||
Ok(if path.is_dir() {
|
||||
Self::Folder {
|
||||
path,
|
||||
name,
|
||||
open: false,
|
||||
root: false,
|
||||
}
|
||||
} else {
|
||||
Self::File { path, name }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn icon(&self, size: u16) -> Option<widget::icon::Icon> {
|
||||
match self {
|
||||
//TODO: different icon for project root?
|
||||
Self::Folder { open, .. } => Some(if *open {
|
||||
widget::icon::from_name("go-down-symbolic")
|
||||
.size(size)
|
||||
.into()
|
||||
} else {
|
||||
widget::icon::from_name("go-next-symbolic")
|
||||
.size(size)
|
||||
.into()
|
||||
}),
|
||||
Self::File { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Folder { name, .. } => name,
|
||||
Self::File { name, .. } => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ProjectNode {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match self {
|
||||
// Folders are always before files
|
||||
Self::Folder { .. } => {
|
||||
if let Self::File { .. } = other {
|
||||
return Ordering::Less;
|
||||
}
|
||||
}
|
||||
// Files are always after folders
|
||||
Self::File { .. } => {
|
||||
if let Self::Folder { .. } = other {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::localize::sorter().compare(self.name(), other.name())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ProjectNode {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue