Implement history, show operations, implement trash
This commit is contained in:
parent
004fd617ea
commit
12a2a39a9f
6 changed files with 344 additions and 119 deletions
|
|
@ -11,6 +11,12 @@ size = Size
|
||||||
|
|
||||||
# Context Pages
|
# Context Pages
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
operations = Operations
|
||||||
|
pending = Pending
|
||||||
|
failed = Failed
|
||||||
|
complete = Complete
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
properties = Properties
|
properties = Properties
|
||||||
|
|
||||||
|
|
|
||||||
10
justfile
10
justfile
|
|
@ -46,9 +46,17 @@ check *args:
|
||||||
# Runs a clippy check with JSON message format
|
# Runs a clippy check with JSON message format
|
||||||
check-json: (check '--message-format=json')
|
check-json: (check '--message-format=json')
|
||||||
|
|
||||||
|
# Developer target
|
||||||
|
dev *args:
|
||||||
|
cargo fmt
|
||||||
|
cargo test
|
||||||
|
cargo build --profile release-with-debug
|
||||||
|
env RUST_LOG=cosmic_files=debug RUST_BACKTRACE=full target/release-with-debug/cosmic-files {{args}}
|
||||||
|
|
||||||
# Run with debug logs
|
# Run with debug logs
|
||||||
run *args:
|
run *args:
|
||||||
env RUST_LOG=cosmic_files=debug RUST_BACKTRACE=full cargo run --release {{args}}
|
cargo build --release
|
||||||
|
env RUST_LOG=cosmic_files=debug RUST_BACKTRACE=full target/release/cosmic-files {{args}}
|
||||||
|
|
||||||
# Installs files
|
# Installs files
|
||||||
install:
|
install:
|
||||||
|
|
|
||||||
185
src/main.rs
185
src/main.rs
|
|
@ -7,15 +7,22 @@ use cosmic::{
|
||||||
cosmic_theme, executor,
|
cosmic_theme, executor,
|
||||||
iced::{
|
iced::{
|
||||||
event,
|
event,
|
||||||
|
futures::SinkExt,
|
||||||
keyboard::{Event as KeyEvent, KeyCode, Modifiers},
|
keyboard::{Event as KeyEvent, KeyCode, Modifiers},
|
||||||
subscription::Subscription,
|
subscription::{self, Subscription},
|
||||||
window, Event, Length, Point,
|
window, Event, Length, Point,
|
||||||
},
|
},
|
||||||
style,
|
style,
|
||||||
widget::{self, segmented_button},
|
widget::{self, segmented_button},
|
||||||
Application, ApplicationExt, Element,
|
Application, ApplicationExt, Element,
|
||||||
};
|
};
|
||||||
use std::{any::TypeId, env, fs, path::PathBuf, process, collections::HashMap};
|
use std::{
|
||||||
|
any::TypeId,
|
||||||
|
collections::{BTreeMap, HashMap},
|
||||||
|
env, fs, io,
|
||||||
|
path::PathBuf,
|
||||||
|
process, time,
|
||||||
|
};
|
||||||
|
|
||||||
use config::{AppTheme, Config, CONFIG_VERSION};
|
use config::{AppTheme, Config, CONFIG_VERSION};
|
||||||
mod config;
|
mod config;
|
||||||
|
|
@ -31,9 +38,10 @@ mod mouse_area;
|
||||||
|
|
||||||
mod mime_icon;
|
mod mime_icon;
|
||||||
|
|
||||||
|
use operation::Operation;
|
||||||
mod operation;
|
mod operation;
|
||||||
|
|
||||||
use tab::{Location, Tab};
|
use tab::{ItemMetadata, Location, Tab};
|
||||||
mod tab;
|
mod tab;
|
||||||
|
|
||||||
/// Runs application with these settings
|
/// Runs application with these settings
|
||||||
|
|
@ -146,8 +154,12 @@ impl Action {
|
||||||
Action::TabNew => Message::TabNew,
|
Action::TabNew => Message::TabNew,
|
||||||
Action::TabNext => Message::TabNext,
|
Action::TabNext => Message::TabNext,
|
||||||
Action::TabPrev => Message::TabPrev,
|
Action::TabPrev => Message::TabPrev,
|
||||||
Action::TabViewGrid => Message::TabMessage(entity_opt, tab::Message::View(tab::View::Grid)),
|
Action::TabViewGrid => {
|
||||||
Action::TabViewList => Message::TabMessage(entity_opt, tab::Message::View(tab::View::List)),
|
Message::TabMessage(entity_opt, tab::Message::View(tab::View::Grid))
|
||||||
|
}
|
||||||
|
Action::TabViewList => {
|
||||||
|
Message::TabMessage(entity_opt, tab::Message::View(tab::View::List))
|
||||||
|
}
|
||||||
Action::WindowClose => Message::WindowClose,
|
Action::WindowClose => Message::WindowClose,
|
||||||
Action::WindowNew => Message::WindowNew,
|
Action::WindowNew => Message::WindowNew,
|
||||||
}
|
}
|
||||||
|
|
@ -168,6 +180,9 @@ pub enum Message {
|
||||||
NewFile(Option<segmented_button::Entity>),
|
NewFile(Option<segmented_button::Entity>),
|
||||||
NewFolder(Option<segmented_button::Entity>),
|
NewFolder(Option<segmented_button::Entity>),
|
||||||
Paste(Option<segmented_button::Entity>),
|
Paste(Option<segmented_button::Entity>),
|
||||||
|
PendingComplete(u64),
|
||||||
|
PendingError(u64, String),
|
||||||
|
PendingProgress(u64, f32),
|
||||||
RestoreFromTrash(Option<segmented_button::Entity>),
|
RestoreFromTrash(Option<segmented_button::Entity>),
|
||||||
SelectAll(Option<segmented_button::Entity>),
|
SelectAll(Option<segmented_button::Entity>),
|
||||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||||
|
|
@ -187,6 +202,7 @@ pub enum Message {
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ContextPage {
|
pub enum ContextPage {
|
||||||
|
Operations,
|
||||||
Properties,
|
Properties,
|
||||||
Settings,
|
Settings,
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +210,7 @@ pub enum ContextPage {
|
||||||
impl ContextPage {
|
impl ContextPage {
|
||||||
fn title(&self) -> String {
|
fn title(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Operations => fl!("operations"),
|
||||||
Self::Properties => fl!("properties"),
|
Self::Properties => fl!("properties"),
|
||||||
Self::Settings => fl!("settings"),
|
Self::Settings => fl!("settings"),
|
||||||
}
|
}
|
||||||
|
|
@ -211,6 +228,10 @@ pub struct App {
|
||||||
context_page: ContextPage,
|
context_page: ContextPage,
|
||||||
key_binds: HashMap<KeyBind, Action>,
|
key_binds: HashMap<KeyBind, Action>,
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
|
pending_operation_id: u64,
|
||||||
|
pending_operations: BTreeMap<u64, (Operation, f32)>,
|
||||||
|
complete_operations: BTreeMap<u64, Operation>,
|
||||||
|
failed_operations: BTreeMap<u64, (Operation, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
|
@ -227,6 +248,15 @@ impl App {
|
||||||
Command::batch([self.update_title(), self.rescan_tab(entity, location)])
|
Command::batch([self.update_title(), self.rescan_tab(entity, location)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn operation(&mut self, operation: Operation) {
|
||||||
|
let id = self.pending_operation_id;
|
||||||
|
self.pending_operation_id += 1;
|
||||||
|
self.pending_operations.insert(id, (operation, 0.0));
|
||||||
|
//TODO: have some button to show current status
|
||||||
|
self.core.window.show_context = true;
|
||||||
|
self.context_page = ContextPage::Operations;
|
||||||
|
}
|
||||||
|
|
||||||
fn rescan_tab(
|
fn rescan_tab(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: segmented_button::Entity,
|
entity: segmented_button::Entity,
|
||||||
|
|
@ -275,6 +305,47 @@ impl App {
|
||||||
self.set_window_title(window_title)
|
self.set_window_title(window_title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn operations(&self) -> Element<Message> {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
|
||||||
|
//TODO: get height from theme?
|
||||||
|
let progress_bar_height = Length::Fixed(4.0);
|
||||||
|
|
||||||
|
if !self.pending_operations.is_empty() {
|
||||||
|
let mut section = widget::settings::view_section(fl!("pending"));
|
||||||
|
for (id, (op, progress)) in self.pending_operations.iter() {
|
||||||
|
section = section.add(widget::column::with_children(vec![
|
||||||
|
widget::text(format!("{:?}", op)).into(),
|
||||||
|
widget::progress_bar(0.0..=100.0, *progress)
|
||||||
|
.height(progress_bar_height)
|
||||||
|
.into(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
children.push(section.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.failed_operations.is_empty() {
|
||||||
|
let mut section = widget::settings::view_section(fl!("failed"));
|
||||||
|
for (id, (op, error)) in self.failed_operations.iter() {
|
||||||
|
section = section.add(widget::column::with_children(vec![
|
||||||
|
widget::text(format!("{:?}", op)).into(),
|
||||||
|
widget::text(error).into(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
children.push(section.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.complete_operations.is_empty() {
|
||||||
|
let mut section = widget::settings::view_section(fl!("complete"));
|
||||||
|
for (id, op) in self.complete_operations.iter() {
|
||||||
|
section = section.add(widget::text(format!("{:?}", op)));
|
||||||
|
}
|
||||||
|
children.push(section.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
widget::settings::view_column(children).into()
|
||||||
|
}
|
||||||
|
|
||||||
fn properties(&self) -> Element<Message> {
|
fn properties(&self) -> Element<Message> {
|
||||||
let mut children = Vec::new();
|
let mut children = Vec::new();
|
||||||
let entity = self.tab_model.active();
|
let entity = self.tab_model.active();
|
||||||
|
|
@ -388,6 +459,10 @@ impl Application for App {
|
||||||
context_page: ContextPage::Settings,
|
context_page: ContextPage::Settings,
|
||||||
key_binds: key_binds(),
|
key_binds: key_binds(),
|
||||||
modifiers: Modifiers::empty(),
|
modifiers: Modifiers::empty(),
|
||||||
|
pending_operation_id: 0,
|
||||||
|
pending_operations: BTreeMap::new(),
|
||||||
|
complete_operations: BTreeMap::new(),
|
||||||
|
failed_operations: BTreeMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut commands = Vec::new();
|
let mut commands = Vec::new();
|
||||||
|
|
@ -507,7 +582,20 @@ impl Application for App {
|
||||||
self.modifiers = modifiers;
|
self.modifiers = modifiers;
|
||||||
}
|
}
|
||||||
Message::MoveToTrash(entity_opt) => {
|
Message::MoveToTrash(entity_opt) => {
|
||||||
log::warn!("TODO: MOVE TO TRASH");
|
let mut paths = Vec::new();
|
||||||
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
|
if let Some(ref mut items) = tab.items_opt {
|
||||||
|
for item in items.iter_mut() {
|
||||||
|
if item.selected {
|
||||||
|
paths.push(item.path.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !paths.is_empty() {
|
||||||
|
self.operation(Operation::Delete { paths });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::NewFile(entity_opt) => {
|
Message::NewFile(entity_opt) => {
|
||||||
log::warn!("TODO: NEW FILE");
|
log::warn!("TODO: NEW FILE");
|
||||||
|
|
@ -518,8 +606,43 @@ impl Application for App {
|
||||||
Message::Paste(entity_opt) => {
|
Message::Paste(entity_opt) => {
|
||||||
log::warn!("TODO: PASTE");
|
log::warn!("TODO: PASTE");
|
||||||
}
|
}
|
||||||
|
Message::PendingComplete(id) => {
|
||||||
|
if let Some((op, _)) = self.pending_operations.remove(&id) {
|
||||||
|
self.complete_operations.insert(id, op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::PendingError(id, err) => {
|
||||||
|
if let Some((op, _)) = self.pending_operations.remove(&id) {
|
||||||
|
self.failed_operations.insert(id, (op, err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::PendingProgress(id, new_progress) => {
|
||||||
|
if let Some((_, progress)) = self.pending_operations.get_mut(&id) {
|
||||||
|
*progress = new_progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::RestoreFromTrash(entity_opt) => {
|
Message::RestoreFromTrash(entity_opt) => {
|
||||||
log::warn!("TODO: RESTORE FROM TRASH");
|
let mut paths = Vec::new();
|
||||||
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
|
if let Some(ref mut items) = tab.items_opt {
|
||||||
|
for item in items.iter_mut() {
|
||||||
|
if item.selected {
|
||||||
|
match &item.metadata {
|
||||||
|
ItemMetadata::Trash { entry, .. } => {
|
||||||
|
paths.push(entry.clone());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
//TODO: error on trying to restore non-trash file?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !paths.is_empty() {
|
||||||
|
self.operation(Operation::Restore { paths });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::SelectAll(entity_opt) => {
|
Message::SelectAll(entity_opt) => {
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
|
|
@ -692,24 +815,18 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(match self.context_page {
|
Some(match self.context_page {
|
||||||
|
ContextPage::Operations => self.operations(),
|
||||||
ContextPage::Properties => self.properties(),
|
ContextPage::Properties => self.properties(),
|
||||||
ContextPage::Settings => self.settings(),
|
ContextPage::Settings => self.settings(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
||||||
vec![
|
vec![menu::menu_bar(&self.key_binds).into()]
|
||||||
menu::menu_bar(&self.key_binds).into(),
|
|
||||||
//TODO: use theme defined space?
|
|
||||||
widget::horizontal_space(Length::Fixed(32.0)).into(),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_end(&self) -> Vec<Element<Self::Message>> {
|
fn header_end(&self) -> Vec<Element<Self::Message>> {
|
||||||
vec![
|
vec![]
|
||||||
//TODO: use defined space
|
|
||||||
widget::horizontal_space(Length::Fixed(32.0)).into(),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a view after each update.
|
/// Creates a view after each update.
|
||||||
|
|
@ -778,14 +895,12 @@ impl Application for App {
|
||||||
struct ConfigSubscription;
|
struct ConfigSubscription;
|
||||||
struct ThemeSubscription;
|
struct ThemeSubscription;
|
||||||
|
|
||||||
Subscription::batch([
|
let mut subscriptions = vec![
|
||||||
event::listen_with(|event, _status| match event {
|
event::listen_with(|event, _status| match event {
|
||||||
Event::Keyboard(KeyEvent::KeyPressed {
|
Event::Keyboard(KeyEvent::KeyPressed {
|
||||||
key_code,
|
key_code,
|
||||||
modifiers,
|
modifiers,
|
||||||
}) => {
|
}) => Some(Message::Key(modifiers, key_code)),
|
||||||
Some(Message::Key(modifiers, key_code))
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
||||||
Some(Message::Modifiers(modifiers))
|
Some(Message::Modifiers(modifiers))
|
||||||
}
|
}
|
||||||
|
|
@ -821,6 +936,34 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
Message::SystemThemeModeChange(update.config)
|
Message::SystemThemeModeChange(update.config)
|
||||||
}),
|
}),
|
||||||
])
|
];
|
||||||
|
|
||||||
|
for (id, (pending_operation, _)) in self.pending_operations.iter() {
|
||||||
|
//TODO: use recipe?
|
||||||
|
let id = *id;
|
||||||
|
let pending_operation = pending_operation.clone();
|
||||||
|
subscriptions.push(subscription::channel(
|
||||||
|
id,
|
||||||
|
16,
|
||||||
|
move |mut msg_tx| async move {
|
||||||
|
match pending_operation.perform(id, &mut msg_tx).await {
|
||||||
|
Ok(()) => {
|
||||||
|
msg_tx.send(Message::PendingComplete(id)).await;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
msg_tx
|
||||||
|
.send(Message::PendingError(id, err.to_string()))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(time::Duration::new(1, 0)).await;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Subscription::batch(subscriptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
src/menu.rs
17
src/menu.rs
|
|
@ -13,7 +13,7 @@ use cosmic::{
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{fl, KeyBind, tab, Action, ContextPage, Location, Message, Tab};
|
use crate::{fl, tab, Action, ContextPage, KeyBind, Location, Message, Tab};
|
||||||
|
|
||||||
macro_rules! menu_button {
|
macro_rules! menu_button {
|
||||||
($($x:expr),+ $(,)?) => (
|
($($x:expr),+ $(,)?) => (
|
||||||
|
|
@ -144,19 +144,10 @@ pub fn menu_bar<'a>(key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message
|
||||||
MenuTree::with_children(
|
MenuTree::with_children(
|
||||||
menu_root(fl!("view")),
|
menu_root(fl!("view")),
|
||||||
vec![
|
vec![
|
||||||
menu_item(
|
menu_item(fl!("grid-view"), Action::TabViewGrid),
|
||||||
fl!("grid-view"),
|
menu_item(fl!("list-view"), Action::TabViewList),
|
||||||
Action::TabViewGrid
|
|
||||||
),
|
|
||||||
menu_item(
|
|
||||||
fl!("list-view"),
|
|
||||||
Action::TabViewList
|
|
||||||
),
|
|
||||||
MenuTree::new(horizontal_rule(1)),
|
MenuTree::new(horizontal_rule(1)),
|
||||||
menu_item(
|
menu_item(fl!("menu-settings"), Action::Settings),
|
||||||
fl!("menu-settings"),
|
|
||||||
Action::Settings,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
|
|
||||||
108
src/operation.rs
108
src/operation.rs
|
|
@ -1,57 +1,73 @@
|
||||||
use std::path::PathBuf;
|
use cosmic::iced::futures::{channel::mpsc, SinkExt};
|
||||||
|
use std::{error::Error, future::Future, io, path::PathBuf, time};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
use crate::Message;
|
||||||
|
|
||||||
|
fn err_str<T: ToString>(err: T) -> String {
|
||||||
|
err.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
/// Move a path to the trash
|
/// Copy items
|
||||||
Delete { path: PathBuf },
|
Copy { paths: Vec<PathBuf>, to: PathBuf },
|
||||||
/// Rename a path
|
/// Move items to the trash
|
||||||
Rename { old: PathBuf, new: PathBuf },
|
Delete { paths: Vec<PathBuf> },
|
||||||
|
/// Move items
|
||||||
|
Move { paths: Vec<PathBuf>, to: PathBuf },
|
||||||
/// Restore a path from the trash
|
/// Restore a path from the trash
|
||||||
Restore { path: PathBuf },
|
Restore { paths: Vec<trash::TrashItem> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation {
|
impl Operation {
|
||||||
pub fn delete(path: impl Into<PathBuf>) -> Self {
|
/// Perform the operation
|
||||||
Self::Delete { path: path.into() }
|
pub async fn perform(self, id: u64, msg_tx: &mut mpsc::Sender<Message>) -> Result<(), String> {
|
||||||
}
|
msg_tx.send(Message::PendingProgress(id, 0.0)).await;
|
||||||
|
|
||||||
pub fn rename(old: impl Into<PathBuf>, new: impl Into<PathBuf>) -> Self {
|
//TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE
|
||||||
Self::Rename {
|
//TODO: SAFELY HANDLE CANCEL
|
||||||
old: old.into(),
|
|
||||||
new: new.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn restore(path: impl Into<PathBuf>) -> Self {
|
|
||||||
Self::Restore { path: path.into() }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reverse(self) -> Self {
|
|
||||||
match self {
|
match self {
|
||||||
Self::Delete { path } => Self::Restore { path },
|
Self::Delete { paths } => {
|
||||||
Self::Rename { old, new } => Self::Rename { old: new, new: old },
|
let mut total = paths.len();
|
||||||
Self::Restore { path } => Self::Delete { path },
|
let mut count = 0;
|
||||||
|
for path in paths {
|
||||||
|
tokio::task::spawn_blocking(|| trash::delete(path))
|
||||||
|
.await
|
||||||
|
.map_err(err_str)?
|
||||||
|
.map_err(err_str)?;
|
||||||
|
count += 1;
|
||||||
|
msg_tx
|
||||||
|
.send(Message::PendingProgress(
|
||||||
|
id,
|
||||||
|
100.0 * (count as f32) / (total as f32),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Restore { paths } => {
|
||||||
|
let mut total = paths.len();
|
||||||
|
let mut count = 0;
|
||||||
|
for path in paths {
|
||||||
|
tokio::task::spawn_blocking(|| trash::os_limited::restore_all([path]))
|
||||||
|
.await
|
||||||
|
.map_err(err_str)?
|
||||||
|
.map_err(err_str)?;
|
||||||
|
count += 1;
|
||||||
|
msg_tx
|
||||||
|
.send(Message::PendingProgress(
|
||||||
|
id,
|
||||||
|
100.0 * (count as f32) / (total as f32),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err("not implemented".to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
msg_tx.send(Message::PendingProgress(id, 100.0)).await;
|
||||||
|
|
||||||
#[cfg(test)]
|
Ok(())
|
||||||
mod tests {
|
|
||||||
use super::Operation;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn operation() {
|
|
||||||
assert_eq!(
|
|
||||||
Operation::delete("foo").reverse(),
|
|
||||||
Operation::restore("foo")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Operation::rename("foo", "bar").reverse(),
|
|
||||||
Operation::rename("bar", "foo")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Operation::restore("foo").reverse(),
|
|
||||||
Operation::delete("foo")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
137
src/tab.rs
137
src/tab.rs
|
|
@ -255,7 +255,7 @@ pub fn scan_path(tab_path: &PathBuf) -> Vec<Item> {
|
||||||
|
|
||||||
items.push(Item {
|
items.push(Item {
|
||||||
name,
|
name,
|
||||||
metadata: ItemMetadata::Path(metadata, children),
|
metadata: ItemMetadata::Path { metadata, children },
|
||||||
hidden,
|
hidden,
|
||||||
path,
|
path,
|
||||||
icon_handle_grid,
|
icon_handle_grid,
|
||||||
|
|
@ -316,7 +316,7 @@ pub fn scan_trash() -> Vec<Item> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = entry.original_path();
|
let path = entry.original_path();
|
||||||
let name = entry.name;
|
let name = entry.name.clone();
|
||||||
|
|
||||||
//TODO: configurable size
|
//TODO: configurable size
|
||||||
let (icon_handle_grid, icon_handle_list) = match metadata.size {
|
let (icon_handle_grid, icon_handle_list) = match metadata.size {
|
||||||
|
|
@ -332,7 +332,7 @@ pub fn scan_trash() -> Vec<Item> {
|
||||||
|
|
||||||
items.push(Item {
|
items.push(Item {
|
||||||
name,
|
name,
|
||||||
metadata: ItemMetadata::Trash(metadata),
|
metadata: ItemMetadata::Trash { metadata, entry },
|
||||||
hidden: false,
|
hidden: false,
|
||||||
path,
|
path,
|
||||||
icon_handle_grid,
|
icon_handle_grid,
|
||||||
|
|
@ -373,6 +373,8 @@ impl Location {
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
Click(Option<usize>),
|
Click(Option<usize>),
|
||||||
EditLocation(Option<Location>),
|
EditLocation(Option<Location>),
|
||||||
|
GoNext,
|
||||||
|
GoPrevious,
|
||||||
Location(Location),
|
Location(Location),
|
||||||
RightClick(usize),
|
RightClick(usize),
|
||||||
View(View),
|
View(View),
|
||||||
|
|
@ -380,15 +382,21 @@ pub enum Message {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ItemMetadata {
|
pub enum ItemMetadata {
|
||||||
Path(Metadata, usize),
|
Path {
|
||||||
Trash(trash::TrashItemMetadata),
|
metadata: Metadata,
|
||||||
|
children: usize,
|
||||||
|
},
|
||||||
|
Trash {
|
||||||
|
metadata: trash::TrashItemMetadata,
|
||||||
|
entry: trash::TrashItem,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ItemMetadata {
|
impl ItemMetadata {
|
||||||
pub fn is_dir(&self) -> bool {
|
pub fn is_dir(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Path(metadata, _) => metadata.is_dir(),
|
Self::Path { metadata, .. } => metadata.is_dir(),
|
||||||
Self::Trash(metadata) => match metadata.size {
|
Self::Trash { metadata, .. } => match metadata.size {
|
||||||
trash::TrashItemSize::Entries(_) => true,
|
trash::TrashItemSize::Entries(_) => true,
|
||||||
trash::TrashItemSize::Bytes(_) => false,
|
trash::TrashItemSize::Bytes(_) => false,
|
||||||
},
|
},
|
||||||
|
|
@ -421,7 +429,7 @@ impl Item {
|
||||||
//TODO: translate!
|
//TODO: translate!
|
||||||
//TODO: correct display of folder size?
|
//TODO: correct display of folder size?
|
||||||
match &self.metadata {
|
match &self.metadata {
|
||||||
ItemMetadata::Path(metadata, children) => {
|
ItemMetadata::Path { metadata, children } => {
|
||||||
if metadata.is_dir() {
|
if metadata.is_dir() {
|
||||||
section = section.add(widget::settings::item::item(
|
section = section.add(widget::settings::item::item(
|
||||||
"Items",
|
"Items",
|
||||||
|
|
@ -467,7 +475,7 @@ impl Item {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ItemMetadata::Trash(_metadata) => {
|
ItemMetadata::Trash { .. } => {
|
||||||
//TODO: trash metadata
|
//TODO: trash metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -504,16 +512,21 @@ pub struct Tab {
|
||||||
pub items_opt: Option<Vec<Item>>,
|
pub items_opt: Option<Vec<Item>>,
|
||||||
pub view: View,
|
pub view: View,
|
||||||
pub edit_location: Option<Location>,
|
pub edit_location: Option<Location>,
|
||||||
|
pub history_i: usize,
|
||||||
|
pub history: Vec<Location>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
pub fn new(location: Location) -> Self {
|
pub fn new(location: Location) -> Self {
|
||||||
|
let history = vec![location.clone()];
|
||||||
Self {
|
Self {
|
||||||
location,
|
location,
|
||||||
context_menu: None,
|
context_menu: None,
|
||||||
items_opt: None,
|
items_opt: None,
|
||||||
view: View::List,
|
view: View::List,
|
||||||
edit_location: None,
|
edit_location: None,
|
||||||
|
history_i: 0,
|
||||||
|
history,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -531,6 +544,7 @@ impl Tab {
|
||||||
|
|
||||||
pub fn update(&mut self, message: Message, modifiers: Modifiers) -> bool {
|
pub fn update(&mut self, message: Message, modifiers: Modifiers) -> bool {
|
||||||
let mut cd = None;
|
let mut cd = None;
|
||||||
|
let mut history_i_opt = None;
|
||||||
match message {
|
match message {
|
||||||
Message::Click(click_i_opt) => {
|
Message::Click(click_i_opt) => {
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
|
|
@ -579,6 +593,22 @@ impl Tab {
|
||||||
Message::EditLocation(edit_location) => {
|
Message::EditLocation(edit_location) => {
|
||||||
self.edit_location = edit_location;
|
self.edit_location = edit_location;
|
||||||
}
|
}
|
||||||
|
Message::GoNext => {
|
||||||
|
if let Some(history_i) = self.history_i.checked_add(1) {
|
||||||
|
if let Some(location) = self.history.get(history_i) {
|
||||||
|
cd = Some(location.clone());
|
||||||
|
history_i_opt = Some(history_i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::GoPrevious => {
|
||||||
|
if let Some(history_i) = self.history_i.checked_sub(1) {
|
||||||
|
if let Some(location) = self.history.get(history_i) {
|
||||||
|
cd = Some(location.clone());
|
||||||
|
history_i_opt = Some(history_i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::Location(location) => {
|
Message::Location(location) => {
|
||||||
cd = Some(location);
|
cd = Some(location);
|
||||||
}
|
}
|
||||||
|
|
@ -603,11 +633,22 @@ impl Tab {
|
||||||
self.view = view;
|
self.view = view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(location) = cd {
|
if let Some(mut location) = cd {
|
||||||
if location != self.location {
|
if location != self.location {
|
||||||
self.location = location;
|
self.location = location.clone();
|
||||||
self.items_opt = None;
|
self.items_opt = None;
|
||||||
self.edit_location = None;
|
self.edit_location = None;
|
||||||
|
if let Some(history_i) = history_i_opt {
|
||||||
|
// Navigating in history
|
||||||
|
self.history_i = history_i;
|
||||||
|
} else {
|
||||||
|
// Truncate history to remove next entries
|
||||||
|
self.history.truncate(self.history_i + 1);
|
||||||
|
|
||||||
|
// Push to the front of history
|
||||||
|
self.history_i = self.history.len();
|
||||||
|
self.history.push(location);
|
||||||
|
}
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
@ -617,36 +658,64 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn breadcrumbs_view(&self, core: &Core) -> Element<Message> {
|
pub fn location_view(&self, core: &Core) -> Element<Message> {
|
||||||
let cosmic_theme::Spacing {
|
let cosmic_theme::Spacing {
|
||||||
space_xxxs,
|
space_xxxs,
|
||||||
space_xxs,
|
space_xxs,
|
||||||
|
space_s,
|
||||||
..
|
..
|
||||||
} = core.system_theme().cosmic().spacing;
|
} = core.system_theme().cosmic().spacing;
|
||||||
|
|
||||||
|
let mut row = widget::row::with_capacity(5).align_items(Alignment::Center);
|
||||||
|
|
||||||
|
let mut prev_button =
|
||||||
|
widget::button(widget::icon::from_name("go-previous-symbolic").size(16))
|
||||||
|
.padding(space_xxs)
|
||||||
|
.style(theme::Button::Icon);
|
||||||
|
if self.history_i > 0 && !self.history.is_empty() {
|
||||||
|
prev_button = prev_button.on_press(Message::GoPrevious);
|
||||||
|
}
|
||||||
|
row = row.push(prev_button);
|
||||||
|
|
||||||
|
let mut next_button = widget::button(widget::icon::from_name("go-next-symbolic").size(16))
|
||||||
|
.padding(space_xxs)
|
||||||
|
.style(theme::Button::Icon);
|
||||||
|
if self.history_i + 1 < self.history.len() {
|
||||||
|
next_button = next_button.on_press(Message::GoNext);
|
||||||
|
}
|
||||||
|
row = row.push(next_button);
|
||||||
|
|
||||||
|
row = row.push(widget::horizontal_space(Length::Fixed(space_s.into())));
|
||||||
|
|
||||||
if let Some(location) = &self.edit_location {
|
if let Some(location) = &self.edit_location {
|
||||||
match location {
|
match location {
|
||||||
Location::Path(path) => {
|
Location::Path(path) => {
|
||||||
return widget::row::with_children(vec![
|
row = row.push(
|
||||||
widget::button(widget::icon::from_name("window-close-symbolic").size(16))
|
widget::button(widget::icon::from_name("window-close-symbolic").size(16))
|
||||||
.on_press(Message::EditLocation(None))
|
.on_press(Message::EditLocation(None))
|
||||||
.padding(space_xxs)
|
.padding(space_xxs)
|
||||||
.style(theme::Button::Icon)
|
.style(theme::Button::Icon),
|
||||||
.into(),
|
);
|
||||||
|
row = row.push(
|
||||||
widget::text_input("", path.to_string_lossy())
|
widget::text_input("", path.to_string_lossy())
|
||||||
.on_input(|input| {
|
.on_input(|input| {
|
||||||
Message::EditLocation(Some(Location::Path(PathBuf::from(input))))
|
Message::EditLocation(Some(Location::Path(PathBuf::from(input))))
|
||||||
})
|
})
|
||||||
.on_submit(Message::Location(location.clone()))
|
.on_submit(Message::Location(location.clone())),
|
||||||
.into(),
|
);
|
||||||
])
|
return row.into();
|
||||||
.align_items(Alignment::Center)
|
|
||||||
.into();
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
//TODO: allow editing other locations
|
//TODO: allow editing other locations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
row = row.push(
|
||||||
|
widget::button(widget::icon::from_name("edit-symbolic").size(16))
|
||||||
|
.on_press(Message::EditLocation(Some(self.location.clone())))
|
||||||
|
.padding(space_xxs)
|
||||||
|
.style(theme::Button::Icon),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut children: Vec<Element<_>> = Vec::new();
|
let mut children: Vec<Element<_>> = Vec::new();
|
||||||
|
|
@ -726,18 +795,10 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
children.insert(
|
for child in children {
|
||||||
0,
|
row = row.push(child);
|
||||||
widget::button(widget::icon::from_name("edit-symbolic").size(16))
|
}
|
||||||
.on_press(Message::EditLocation(Some(self.location.clone())))
|
row.into()
|
||||||
.padding(space_xxs)
|
|
||||||
.style(theme::Button::Icon)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
widget::row::with_children(children)
|
|
||||||
.align_items(Alignment::Center)
|
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn empty_view(&self, has_hidden: bool, core: &Core) -> Element<Message> {
|
pub fn empty_view(&self, has_hidden: bool, core: &Core) -> Element<Message> {
|
||||||
|
|
@ -815,7 +876,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
widget::scrollable(widget::column::with_children(vec![
|
widget::scrollable(widget::column::with_children(vec![
|
||||||
self.breadcrumbs_view(core),
|
self.location_view(core),
|
||||||
widget::flex_row(children).into(),
|
widget::flex_row(children).into(),
|
||||||
]))
|
]))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|
@ -830,7 +891,7 @@ impl Tab {
|
||||||
|
|
||||||
let mut children: Vec<Element<_>> = Vec::new();
|
let mut children: Vec<Element<_>> = Vec::new();
|
||||||
|
|
||||||
children.push(self.breadcrumbs_view(core));
|
children.push(self.location_view(core));
|
||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
widget::row::with_children(vec![
|
widget::row::with_children(vec![
|
||||||
|
|
@ -868,24 +929,24 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
let modified_text = match &item.metadata {
|
let modified_text = match &item.metadata {
|
||||||
ItemMetadata::Path(metadata, _children) => match metadata.modified() {
|
ItemMetadata::Path { metadata, .. } => match metadata.modified() {
|
||||||
Ok(time) => chrono::DateTime::<chrono::Local>::from(time)
|
Ok(time) => chrono::DateTime::<chrono::Local>::from(time)
|
||||||
.format("%c")
|
.format("%c")
|
||||||
.to_string(),
|
.to_string(),
|
||||||
Err(_) => String::new(),
|
Err(_) => String::new(),
|
||||||
},
|
},
|
||||||
ItemMetadata::Trash(metadata) => String::new(),
|
ItemMetadata::Trash { .. } => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let size_text = match &item.metadata {
|
let size_text = match &item.metadata {
|
||||||
ItemMetadata::Path(metadata, children) => {
|
ItemMetadata::Path { metadata, children } => {
|
||||||
if metadata.is_dir() {
|
if metadata.is_dir() {
|
||||||
format!("{} items", children)
|
format!("{} items", children)
|
||||||
} else {
|
} else {
|
||||||
format_size(metadata.len())
|
format_size(metadata.len())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ItemMetadata::Trash(metadata) => match metadata.size {
|
ItemMetadata::Trash { metadata, .. } => match metadata.size {
|
||||||
trash::TrashItemSize::Entries(entries) => {
|
trash::TrashItemSize::Entries(entries) => {
|
||||||
//TODO: translate
|
//TODO: translate
|
||||||
if entries == 1 {
|
if entries == 1 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue