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",
|
||||||
"i18n-embed-fl",
|
"i18n-embed-fl",
|
||||||
"iced_video_player",
|
"iced_video_player",
|
||||||
|
"icu_collator",
|
||||||
|
"icu_provider",
|
||||||
"image",
|
"image",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
|
|
@ -2484,9 +2486,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "i18n-embed"
|
name = "i18n-embed"
|
||||||
version = "0.13.9"
|
version = "0.14.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92a86226a7a16632de6723449ee5fe70bac5af718bc642ee9ca2f0f6e14fa1fa"
|
checksum = "94205d95764f5bb9db9ea98fa77f89653365ca748e27161f5bbea2ffd50e459c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"fluent",
|
"fluent",
|
||||||
|
|
@ -2506,9 +2508,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "i18n-embed-fl"
|
name = "i18n-embed-fl"
|
||||||
version = "0.6.7"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d26a3d3569737dfaac7fc1c4078e6af07471c3060b8e570bcd83cdd5f4685395"
|
checksum = "9fc1f8715195dffc4caddcf1cf3128da15fe5d8a137606ea8856c9300047d5a2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"find-crate",
|
"find-crate",
|
||||||
|
|
@ -2797,6 +2799,31 @@ dependencies = [
|
||||||
"objc2 0.5.2",
|
"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]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -4442,9 +4469,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed"
|
name = "rust-embed"
|
||||||
version = "6.8.1"
|
version = "8.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
|
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rust-embed-impl",
|
"rust-embed-impl",
|
||||||
"rust-embed-utils",
|
"rust-embed-utils",
|
||||||
|
|
@ -4453,9 +4480,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed-impl"
|
name = "rust-embed-impl"
|
||||||
version = "6.8.1"
|
version = "8.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
|
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -4466,9 +4493,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed-utils"
|
name = "rust-embed-utils"
|
||||||
version = "7.8.1"
|
version = "8.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
|
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@ tempfile = "3"
|
||||||
tokio = "1"
|
tokio = "1"
|
||||||
url = "2"
|
url = "2"
|
||||||
# Internationalization
|
# Internationalization
|
||||||
i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }
|
icu_collator = "1.5"
|
||||||
i18n-embed-fl = "0.6"
|
icu_provider = { version = "1.5", features = ["sync"] }
|
||||||
rust-embed = "6"
|
i18n-embed = { version = "0.14", features = ["fluent-system", "desktop-requester"] }
|
||||||
|
i18n-embed-fl = "0.7"
|
||||||
|
rust-embed = "8"
|
||||||
# Logging
|
# Logging
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
audio = Audio
|
audio = Audio
|
||||||
no-video-or-audio-file-open = No video or audio file open
|
no-video-or-audio-file-open = No video or audio file open
|
||||||
open-file = Open file
|
open-file = Open file
|
||||||
|
open-folder = Open folder
|
||||||
subtitles = Subtitles
|
subtitles = Subtitles
|
||||||
|
|
||||||
# Context Pages
|
# Context Pages
|
||||||
|
|
@ -24,4 +25,5 @@ open-recent-media = Open recent media
|
||||||
close-file = Close file
|
close-file = Close file
|
||||||
open-media-folder = Open media folder...
|
open-media-folder = Open media folder...
|
||||||
open-recent-media-folder = Open recent media folder
|
open-recent-media-folder = Open recent media folder
|
||||||
|
close-media-folder = Close media folder
|
||||||
quit = Quit
|
quit = Quit
|
||||||
|
|
@ -5,7 +5,7 @@ use cosmic::{
|
||||||
theme,
|
theme,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::VecDeque;
|
use std::{collections::VecDeque, path::PathBuf};
|
||||||
|
|
||||||
pub const CONFIG_VERSION: u64 = 1;
|
pub const CONFIG_VERSION: u64 = 1;
|
||||||
|
|
||||||
|
|
@ -43,14 +43,14 @@ impl Default for Config {
|
||||||
#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub struct ConfigState {
|
pub struct ConfigState {
|
||||||
pub recent_files: VecDeque<url::Url>,
|
pub recent_files: VecDeque<url::Url>,
|
||||||
pub recent_folders: VecDeque<url::Url>,
|
pub recent_projects: VecDeque<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ConfigState {
|
impl Default for ConfigState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
recent_files: VecDeque::new(),
|
recent_files: VecDeque::new(),
|
||||||
recent_folders: VecDeque::new(),
|
recent_projects: VecDeque::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,37 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use i18n_embed::{
|
use i18n_embed::{
|
||||||
fluent::{fluent_language_loader, FluentLanguageLoader},
|
fluent::{fluent_language_loader, FluentLanguageLoader},
|
||||||
DefaultLocalizer, LanguageLoader, Localizer,
|
DefaultLocalizer, LanguageLoader, Localizer,
|
||||||
};
|
};
|
||||||
|
use icu_collator::{Collator, CollatorOptions, Numeric};
|
||||||
|
use icu_provider::DataLocale;
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
#[folder = "i18n/"]
|
#[folder = "i18n/"]
|
||||||
struct Localizations;
|
struct Localizations;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
pub static LANGUAGE_LOADER: OnceLock<FluentLanguageLoader> = OnceLock::new();
|
||||||
pub static ref LANGUAGE_LOADER: FluentLanguageLoader = {
|
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!();
|
let loader: FluentLanguageLoader = fluent_language_loader!();
|
||||||
|
|
||||||
loader
|
loader
|
||||||
|
|
@ -19,23 +39,12 @@ lazy_static::lazy_static! {
|
||||||
.expect("Error while loading fallback language");
|
.expect("Error while loading fallback language");
|
||||||
|
|
||||||
loader
|
loader
|
||||||
};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
Box::from(DefaultLocalizer::new(
|
||||||
macro_rules! fl {
|
LANGUAGE_LOADER.get().unwrap(),
|
||||||
($message_id:literal) => {{
|
&Localizations,
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn localize() {
|
pub fn localize() {
|
||||||
|
|
@ -46,3 +55,22 @@ pub fn localize() {
|
||||||
eprintln!("Error while loading language for App List {}", error);
|
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,
|
subscription::Subscription,
|
||||||
window, Alignment, Background, Border, Color, ContentFit, Length, Limits,
|
window, Alignment, Background, Border, Color, ContentFit, Length, Limits,
|
||||||
},
|
},
|
||||||
theme,
|
iced_style, theme,
|
||||||
widget::{self, menu::action::MenuAction, Slider},
|
widget::{self, menu::action::MenuAction, nav_bar, segmented_button, Slider},
|
||||||
Application, ApplicationExt, Element,
|
Application, ApplicationExt, Element,
|
||||||
};
|
};
|
||||||
use iced_video_player::{
|
use iced_video_player::{
|
||||||
|
|
@ -24,7 +24,9 @@ use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
ffi::{CStr, CString},
|
ffi::{CStr, CString},
|
||||||
fs, process, thread,
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process, thread,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
@ -32,6 +34,7 @@ use tokio::sync::mpsc;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, ConfigState, CONFIG_VERSION},
|
config::{Config, ConfigState, CONFIG_VERSION},
|
||||||
key_bind::{key_binds, KeyBind},
|
key_bind::{key_binds, KeyBind},
|
||||||
|
project::ProjectNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
|
@ -40,6 +43,7 @@ mod localize;
|
||||||
mod menu;
|
mod menu;
|
||||||
#[cfg(feature = "mpris-server")]
|
#[cfg(feature = "mpris-server")]
|
||||||
mod mpris;
|
mod mpris;
|
||||||
|
mod project;
|
||||||
|
|
||||||
static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0);
|
static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0);
|
||||||
|
|
||||||
|
|
@ -143,6 +147,7 @@ pub enum Action {
|
||||||
FileClose,
|
FileClose,
|
||||||
FileOpen,
|
FileOpen,
|
||||||
FileOpenRecent(usize),
|
FileOpenRecent(usize),
|
||||||
|
FolderClose(usize),
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
FolderOpenRecent(usize),
|
FolderOpenRecent(usize),
|
||||||
Fullscreen,
|
Fullscreen,
|
||||||
|
|
@ -160,6 +165,7 @@ impl MenuAction for Action {
|
||||||
Self::FileClose => Message::FileClose,
|
Self::FileClose => Message::FileClose,
|
||||||
Self::FileOpen => Message::FileOpen,
|
Self::FileOpen => Message::FileOpen,
|
||||||
Self::FileOpenRecent(index) => Message::FileOpenRecent(*index),
|
Self::FileOpenRecent(index) => Message::FileOpenRecent(*index),
|
||||||
|
Self::FolderClose(index) => Message::FolderClose(*index),
|
||||||
Self::FolderOpen => Message::FolderOpen,
|
Self::FolderOpen => Message::FolderOpen,
|
||||||
Self::FolderOpenRecent(index) => Message::FolderOpenRecent(*index),
|
Self::FolderOpenRecent(index) => Message::FolderOpenRecent(*index),
|
||||||
Self::Fullscreen => Message::Fullscreen,
|
Self::Fullscreen => Message::Fullscreen,
|
||||||
|
|
@ -224,6 +230,8 @@ pub enum Message {
|
||||||
FileLoad(url::Url),
|
FileLoad(url::Url),
|
||||||
FileOpen,
|
FileOpen,
|
||||||
FileOpenRecent(usize),
|
FileOpenRecent(usize),
|
||||||
|
FolderClose(usize),
|
||||||
|
FolderLoad(PathBuf),
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
FolderOpenRecent(usize),
|
FolderOpenRecent(usize),
|
||||||
Fullscreen,
|
Fullscreen,
|
||||||
|
|
@ -259,6 +267,8 @@ pub struct App {
|
||||||
fullscreen: bool,
|
fullscreen: bool,
|
||||||
key_binds: HashMap<KeyBind, Action>,
|
key_binds: HashMap<KeyBind, Action>,
|
||||||
mpris_opt: Option<(MprisMeta, MprisState, mpsc::UnboundedSender<MprisEvent>)>,
|
mpris_opt: Option<(MprisMeta, MprisState, mpsc::UnboundedSender<MprisEvent>)>,
|
||||||
|
nav_model: segmented_button::SingleSelectModel,
|
||||||
|
projects: Vec<(String, PathBuf)>,
|
||||||
video_opt: Option<Video>,
|
video_opt: Option<Video>,
|
||||||
position: f64,
|
position: f64,
|
||||||
duration: f64,
|
duration: f64,
|
||||||
|
|
@ -293,6 +303,7 @@ impl App {
|
||||||
self.text_codes.clear();
|
self.text_codes.clear();
|
||||||
self.current_text = -1;
|
self.current_text = -1;
|
||||||
self.update_mpris_meta();
|
self.update_mpris_meta();
|
||||||
|
self.update_nav_bar_active();
|
||||||
was_open
|
was_open
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,6 +435,128 @@ impl App {
|
||||||
self.update_title()
|
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) {
|
fn save_config_state(&mut self) {
|
||||||
if let Some(ref config_state_handler) = self.flags.config_state_handler {
|
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) {
|
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> {
|
fn update_title(&mut self) -> Command<Message> {
|
||||||
//TODO: filename?
|
//TODO: filename?
|
||||||
let title = "COSMIC Media Player";
|
let title = "COSMIC Media Player";
|
||||||
|
|
@ -604,6 +783,8 @@ impl Application for App {
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
key_binds: key_binds(),
|
key_binds: key_binds(),
|
||||||
mpris_opt: None,
|
mpris_opt: None,
|
||||||
|
nav_model: nav_bar::Model::builder().build(),
|
||||||
|
projects: Vec::new(),
|
||||||
video_opt: None,
|
video_opt: None,
|
||||||
position: 0.0,
|
position: 0.0,
|
||||||
duration: 0.0,
|
duration: 0.0,
|
||||||
|
|
@ -615,10 +796,25 @@ impl Application for App {
|
||||||
current_text: -1,
|
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();
|
let command = app.load();
|
||||||
(app, command)
|
(app, command)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn nav_model(&self) -> Option<&nav_bar::Model> {
|
||||||
|
Some(&self.nav_model)
|
||||||
|
}
|
||||||
|
|
||||||
fn on_escape(&mut self) -> Command<Self::Message> {
|
fn on_escape(&mut self) -> Command<Self::Message> {
|
||||||
if self.fullscreen {
|
if self.fullscreen {
|
||||||
return self.update(Message::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.
|
/// Handle application events here.
|
||||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||||
match message {
|
match message {
|
||||||
|
|
@ -678,12 +946,75 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
Message::FileOpenRecent(index) => {
|
Message::FileOpenRecent(index) => {
|
||||||
if let Some(url) = self.flags.config_state.recent_files.get(index) {
|
if let Some(url) = self.flags.config_state.recent_files.get(index) {
|
||||||
self.flags.url_opt = Some(url.clone());
|
return self.update(Message::FileLoad(url.clone()));
|
||||||
return self.load();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::FolderOpen | Message::FolderOpenRecent(..) => {
|
Message::FolderClose(project_i) => {
|
||||||
log::error!("TODO: {:?}", message);
|
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 => {
|
Message::Fullscreen => {
|
||||||
//TODO: cleanest way to close dropdowns
|
//TODO: cleanest way to close dropdowns
|
||||||
|
|
@ -882,6 +1213,7 @@ impl Application for App {
|
||||||
&self.flags.config,
|
&self.flags.config,
|
||||||
&self.flags.config_state,
|
&self.flags.config_state,
|
||||||
&self.key_binds,
|
&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},
|
widget::menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar},
|
||||||
Element,
|
Element,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
use crate::{fl, Action, Config, ConfigState, Message};
|
use crate::{fl, Action, Config, ConfigState, Message};
|
||||||
|
|
||||||
|
|
@ -13,18 +13,20 @@ pub fn menu_bar<'a>(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
config_state: &ConfigState,
|
config_state: &ConfigState,
|
||||||
key_binds: &HashMap<KeyBind, Action>,
|
key_binds: &HashMap<KeyBind, Action>,
|
||||||
|
projects: &Vec<(String, PathBuf)>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let home_dir_opt = dirs::home_dir();
|
let home_dir_opt = dirs::home_dir();
|
||||||
let format_path = |url: &url::Url| -> String {
|
let format_path = |path: &PathBuf| -> String {
|
||||||
match url.to_file_path() {
|
if let Some(home_dir) = &home_dir_opt {
|
||||||
Ok(path) => {
|
if let Ok(part) = path.strip_prefix(home_dir) {
|
||||||
if let Some(home_dir) = &home_dir_opt {
|
return format!("~/{}", part.display());
|
||||||
if let Ok(part) = path.strip_prefix(home_dir) {
|
|
||||||
return format!("~/{}", part.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
path.display().to_string()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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(),
|
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());
|
let mut recent_files = Vec::with_capacity(config_state.recent_files.len());
|
||||||
for (i, path) in config_state.recent_files.iter().enumerate() {
|
for (i, path) in config_state.recent_files.iter().enumerate() {
|
||||||
recent_files.push(menu::Item::Button(
|
recent_files.push(menu::Item::Button(
|
||||||
format_path(path),
|
format_url(path),
|
||||||
Action::FileOpenRecent(i),
|
Action::FileOpenRecent(i),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut recent_folders = Vec::with_capacity(config_state.recent_folders.len());
|
let mut recent_projects = Vec::with_capacity(config_state.recent_projects.len());
|
||||||
for (i, path) in config_state.recent_folders.iter().enumerate() {
|
for (i, path) in config_state.recent_projects.iter().enumerate() {
|
||||||
recent_folders.push(menu::Item::Button(
|
recent_projects.push(menu::Item::Button(
|
||||||
format_path(path),
|
format_path(path),
|
||||||
Action::FolderOpenRecent(i),
|
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(
|
MenuBar::new(vec![menu::Tree::with_children(
|
||||||
menu::root(fl!("file")),
|
menu::root(fl!("file")),
|
||||||
menu::items(
|
menu::items(
|
||||||
|
|
@ -54,12 +64,10 @@ pub fn menu_bar<'a>(
|
||||||
menu::Item::Folder(fl!("open-recent-media"), recent_files),
|
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::Button(fl!("open-media-folder"), Action::FolderOpen),
|
||||||
menu::Item::Folder(fl!("open-recent-media-folder"), recent_folders),
|
menu::Item::Folder(fl!("open-recent-media-folder"), recent_projects),
|
||||||
menu::Item::Folder(fl!("close-media-folder"), close_folders),
|
menu::Item::Folder(fl!("close-media-folder"), close_projects),
|
||||||
menu::Item::Divider,
|
menu::Item::Divider,
|
||||||
*/
|
|
||||||
menu::Item::Button(fl!("quit"), Action::WindowClose),
|
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