Implement apply to all and keep both in replace dialog, fixes #180
This commit is contained in:
parent
97444c3572
commit
14be0d82f9
3 changed files with 160 additions and 68 deletions
|
|
@ -50,6 +50,9 @@ replace-warning = Do you want to replace it with the one you are saving? Replaci
|
||||||
replace-warning-operation = Do you want to replace it? Replacing it will overwrite its content.
|
replace-warning-operation = Do you want to replace it? Replacing it will overwrite its content.
|
||||||
original-file = Original file
|
original-file = Original file
|
||||||
replace-with = Replace with
|
replace-with = Replace with
|
||||||
|
apply-to-all = Apply to all
|
||||||
|
keep-both = Keep both
|
||||||
|
skip = Skip
|
||||||
|
|
||||||
# Context Pages
|
# Context Pages
|
||||||
|
|
||||||
|
|
|
||||||
86
src/app.rs
86
src/app.rs
|
|
@ -234,6 +234,7 @@ pub enum Message {
|
||||||
PendingProgress(u64, f32),
|
PendingProgress(u64, f32),
|
||||||
RescanTrash,
|
RescanTrash,
|
||||||
Rename(Option<Entity>),
|
Rename(Option<Entity>),
|
||||||
|
ReplaceResult(ReplaceResult),
|
||||||
RestoreFromTrash(Option<Entity>),
|
RestoreFromTrash(Option<Entity>),
|
||||||
SearchActivate,
|
SearchActivate,
|
||||||
SearchClear,
|
SearchClear,
|
||||||
|
|
@ -300,6 +301,8 @@ pub enum DialogPage {
|
||||||
Replace {
|
Replace {
|
||||||
from: tab::Item,
|
from: tab::Item,
|
||||||
to: tab::Item,
|
to: tab::Item,
|
||||||
|
multiple: bool,
|
||||||
|
apply_to_all: bool,
|
||||||
tx: mpsc::Sender<ReplaceResult>,
|
tx: mpsc::Sender<ReplaceResult>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -1204,14 +1207,8 @@ impl Application for App {
|
||||||
let to = parent.join(name);
|
let to = parent.join(name);
|
||||||
self.operation(Operation::Rename { from, to });
|
self.operation(Operation::Rename { from, to });
|
||||||
}
|
}
|
||||||
DialogPage::Replace { tx, .. } => {
|
DialogPage::Replace { .. } => {
|
||||||
return Command::perform(
|
log::warn!("replace dialog should be completed with replace result");
|
||||||
async move {
|
|
||||||
let _ = tx.send(ReplaceResult::Replace).await;
|
|
||||||
message::none()
|
|
||||||
},
|
|
||||||
|x| x,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1602,6 +1599,25 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::ReplaceResult(replace_result) => {
|
||||||
|
if let Some(dialog_page) = self.dialog_pages.pop_front() {
|
||||||
|
match dialog_page {
|
||||||
|
DialogPage::Replace { tx, .. } => {
|
||||||
|
return Command::perform(
|
||||||
|
async move {
|
||||||
|
let _ = tx.send(replace_result).await;
|
||||||
|
message::none()
|
||||||
|
},
|
||||||
|
|x| x,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
log::warn!("tried to send replace result to the wrong dialog");
|
||||||
|
self.dialog_pages.push_front(other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::RestoreFromTrash(entity_opt) => {
|
Message::RestoreFromTrash(entity_opt) => {
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
|
|
@ -2246,17 +2262,55 @@ impl Application for App {
|
||||||
.spacing(space_xxs),
|
.spacing(space_xxs),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DialogPage::Replace { from, to, .. } => {
|
DialogPage::Replace {
|
||||||
widget::dialog(fl!("replace-title", filename = to.name.as_str()))
|
from,
|
||||||
|
to,
|
||||||
|
multiple,
|
||||||
|
apply_to_all,
|
||||||
|
tx,
|
||||||
|
} => {
|
||||||
|
let dialog = widget::dialog(fl!("replace-title", filename = to.name.as_str()))
|
||||||
.body(fl!("replace-warning-operation"))
|
.body(fl!("replace-warning-operation"))
|
||||||
.control(to.replace_view(fl!("original-file"), IconSizes::default()))
|
.control(to.replace_view(fl!("original-file"), IconSizes::default()))
|
||||||
.control(from.replace_view(fl!("replace-with"), IconSizes::default()))
|
.control(from.replace_view(fl!("replace-with"), IconSizes::default()))
|
||||||
.primary_action(
|
.primary_action(widget::button::suggested(fl!("replace")).on_press(
|
||||||
widget::button::suggested(fl!("replace")).on_press(Message::DialogComplete),
|
Message::ReplaceResult(ReplaceResult::Replace(*apply_to_all)),
|
||||||
)
|
));
|
||||||
.secondary_action(
|
if *multiple {
|
||||||
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
dialog
|
||||||
)
|
.control(widget::checkbox(
|
||||||
|
fl!("apply-to-all"),
|
||||||
|
*apply_to_all,
|
||||||
|
|apply_to_all| {
|
||||||
|
Message::DialogUpdate(DialogPage::Replace {
|
||||||
|
from: from.clone(),
|
||||||
|
to: to.clone(),
|
||||||
|
multiple: *multiple,
|
||||||
|
apply_to_all,
|
||||||
|
tx: tx.clone(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.secondary_action(
|
||||||
|
widget::button::standard(fl!("skip")).on_press(Message::ReplaceResult(
|
||||||
|
ReplaceResult::Skip(*apply_to_all),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.tertiary_action(
|
||||||
|
widget::button::text(fl!("cancel"))
|
||||||
|
.on_press(Message::ReplaceResult(ReplaceResult::Cancel)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
dialog
|
||||||
|
.secondary_action(
|
||||||
|
widget::button::standard(fl!("cancel"))
|
||||||
|
.on_press(Message::ReplaceResult(ReplaceResult::Cancel)),
|
||||||
|
)
|
||||||
|
.tertiary_action(
|
||||||
|
widget::button::text(fl!("keep-both"))
|
||||||
|
.on_press(Message::ReplaceResult(ReplaceResult::KeepBoth)),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
139
src/operation.rs
139
src/operation.rs
|
|
@ -1,7 +1,7 @@
|
||||||
use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt};
|
use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt};
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
path::PathBuf,
|
path::{Path, PathBuf},
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{self, AtomicU64},
|
atomic::{self, AtomicU64},
|
||||||
Arc,
|
Arc,
|
||||||
|
|
@ -23,6 +23,7 @@ fn handle_replace(
|
||||||
msg_tx: &Arc<Mutex<Sender<Message>>>,
|
msg_tx: &Arc<Mutex<Sender<Message>>>,
|
||||||
file_from: PathBuf,
|
file_from: PathBuf,
|
||||||
file_to: PathBuf,
|
file_to: PathBuf,
|
||||||
|
multiple: bool,
|
||||||
) -> ReplaceResult {
|
) -> ReplaceResult {
|
||||||
let item_from = match tab::item_from_path(file_from, IconSizes::default()) {
|
let item_from = match tab::item_from_path(file_from, IconSizes::default()) {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
|
|
@ -48,6 +49,8 @@ fn handle_replace(
|
||||||
.send(Message::DialogPush(DialogPage::Replace {
|
.send(Message::DialogPush(DialogPage::Replace {
|
||||||
from: item_from,
|
from: item_from,
|
||||||
to: item_to,
|
to: item_to,
|
||||||
|
multiple,
|
||||||
|
apply_to_all: false,
|
||||||
tx,
|
tx,
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
|
|
@ -73,7 +76,12 @@ fn handle_progress_state(
|
||||||
return fs_extra::dir::TransitProcessResult::Abort;
|
return fs_extra::dir::TransitProcessResult::Abort;
|
||||||
};
|
};
|
||||||
|
|
||||||
handle_replace(msg_tx, file_from, file_to).into()
|
if file_from == file_to {
|
||||||
|
log::warn!("trying to copy {:?} to itself", file_from);
|
||||||
|
return fs_extra::dir::TransitProcessResult::Abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_replace(msg_tx, file_from, file_to, true).into()
|
||||||
}
|
}
|
||||||
fs_extra::dir::TransitState::NoAccess => {
|
fs_extra::dir::TransitState::NoAccess => {
|
||||||
//TODO: permission error dialog
|
//TODO: permission error dialog
|
||||||
|
|
@ -84,16 +92,33 @@ fn handle_progress_state(
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum ReplaceResult {
|
pub enum ReplaceResult {
|
||||||
Replace,
|
Replace(bool),
|
||||||
Skip,
|
KeepBoth,
|
||||||
|
Skip(bool),
|
||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ReplaceResult> for fs_extra::dir::TransitProcessResult {
|
impl From<ReplaceResult> for fs_extra::dir::TransitProcessResult {
|
||||||
fn from(f: ReplaceResult) -> fs_extra::dir::TransitProcessResult {
|
fn from(f: ReplaceResult) -> fs_extra::dir::TransitProcessResult {
|
||||||
match f {
|
match f {
|
||||||
ReplaceResult::Replace => fs_extra::dir::TransitProcessResult::Overwrite,
|
ReplaceResult::Replace(apply_to_all) => {
|
||||||
ReplaceResult::Skip => fs_extra::dir::TransitProcessResult::Skip,
|
if apply_to_all {
|
||||||
|
fs_extra::dir::TransitProcessResult::OverwriteAll
|
||||||
|
} else {
|
||||||
|
fs_extra::dir::TransitProcessResult::Overwrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReplaceResult::KeepBoth => {
|
||||||
|
log::warn!("tried to keep both when replacing multiple files");
|
||||||
|
fs_extra::dir::TransitProcessResult::Abort
|
||||||
|
}
|
||||||
|
ReplaceResult::Skip(apply_to_all) => {
|
||||||
|
if apply_to_all {
|
||||||
|
fs_extra::dir::TransitProcessResult::SkipAll
|
||||||
|
} else {
|
||||||
|
fs_extra::dir::TransitProcessResult::Skip
|
||||||
|
}
|
||||||
|
}
|
||||||
ReplaceResult::Cancel => fs_extra::dir::TransitProcessResult::Abort,
|
ReplaceResult::Cancel => fs_extra::dir::TransitProcessResult::Abort,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +158,45 @@ pub enum Operation {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
|
||||||
|
let mut to = to.to_owned();
|
||||||
|
// Separate the full file name into its file name plus extension.
|
||||||
|
// `[Path::file_stem]` returns the full name for dotfiles (e.g.
|
||||||
|
// .someconf is the file name)
|
||||||
|
if let (Some(stem), ext) = (
|
||||||
|
// FIXME: Replace `[Path::file_stem]` with `[Path::file_prefix]` when stablized to handle .tar.gz et al. better
|
||||||
|
from.file_stem().and_then(|name| name.to_str()),
|
||||||
|
from.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
// '.' needs to be re-added for paths with extensions.
|
||||||
|
let dot = if ext.is_empty() { "" } else { "." };
|
||||||
|
let mut n = 0u32;
|
||||||
|
// Loop until a valid `copy n` variant is found
|
||||||
|
loop {
|
||||||
|
n = if let Some(n) = n.checked_add(1) {
|
||||||
|
n
|
||||||
|
} else {
|
||||||
|
// TODO: Return error? fs_extra will handle it anyway
|
||||||
|
break to;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rebuild file name
|
||||||
|
let new_name = format!("{stem} ({} {n}){dot}{ext}", fl!("copy_noun"));
|
||||||
|
to = to.join(new_name);
|
||||||
|
|
||||||
|
if !matches!(to.try_exists(), Ok(true)) {
|
||||||
|
break to;
|
||||||
|
}
|
||||||
|
// Continue if a copy with index exists
|
||||||
|
to.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
to
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Operation {
|
impl Operation {
|
||||||
/// Perform the operation
|
/// Perform the operation
|
||||||
pub async fn perform(
|
pub async fn perform(
|
||||||
|
|
@ -159,44 +223,7 @@ impl Operation {
|
||||||
if matches!(from.parent(), Some(parent) if parent == to) {
|
if matches!(from.parent(), Some(parent) if parent == to) {
|
||||||
// `from`'s parent is equal to `to` which means we're copying to the same
|
// `from`'s parent is equal to `to` which means we're copying to the same
|
||||||
// directory (duplicating files)
|
// directory (duplicating files)
|
||||||
let mut to = to.to_owned();
|
let to = copy_unique_path(&from, &to);
|
||||||
// Separate the full file name into its file name plus extension.
|
|
||||||
// `[Path::file_stem]` returns the full name for dotfiles (e.g.
|
|
||||||
// .someconf is the file name)
|
|
||||||
let to = if let (Some(stem), ext) = (
|
|
||||||
// FIXME: Replace `[Path::file_stem]` with `[Path::file_prefix]` when stablized to handle .tar.gz et al. better
|
|
||||||
from.file_stem().and_then(|name| name.to_str()),
|
|
||||||
from.extension()
|
|
||||||
.and_then(|ext| ext.to_str())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
) {
|
|
||||||
// '.' needs to be re-added for paths with extensions.
|
|
||||||
let dot = if ext.is_empty() { "" } else { "." };
|
|
||||||
let mut n = 0u32;
|
|
||||||
// Loop until a valid `copy n` variant is found
|
|
||||||
loop {
|
|
||||||
n = if let Some(n) = n.checked_add(1) {
|
|
||||||
n
|
|
||||||
} else {
|
|
||||||
// TODO: Return error? fs_extra will handle it anyway
|
|
||||||
break to;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Rebuild file name
|
|
||||||
let new_name =
|
|
||||||
format!("{stem} ({} {n}){dot}{ext}", fl!("copy_noun"));
|
|
||||||
to = to.join(new_name);
|
|
||||||
|
|
||||||
if !matches!(to.try_exists(), Ok(true)) {
|
|
||||||
break to;
|
|
||||||
}
|
|
||||||
// Continue if a copy with index exists
|
|
||||||
to.pop();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
to
|
|
||||||
};
|
|
||||||
|
|
||||||
(from, to)
|
(from, to)
|
||||||
} else if let Some(name) =
|
} else if let Some(name) =
|
||||||
from.is_file().then(|| from.file_name()).flatten()
|
from.is_file().then(|| from.file_name()).flatten()
|
||||||
|
|
@ -243,26 +270,34 @@ impl Operation {
|
||||||
handler();
|
handler();
|
||||||
handle_progress_state(&msg_tx, &progress)
|
handle_progress_state(&msg_tx, &progress)
|
||||||
};
|
};
|
||||||
for (from, to) in paths.into_iter().zip(to.into_iter()) {
|
for (from, mut to) in paths.into_iter().zip(to.into_iter()) {
|
||||||
if from.is_dir() {
|
if from.is_dir() {
|
||||||
let options = fs_extra::dir::CopyOptions::default().copy_inside(true);
|
let options = fs_extra::dir::CopyOptions::default().copy_inside(true);
|
||||||
fs_extra::copy_items_with_progress(&[from], to, &options, dir_handler)?;
|
fs_extra::copy_items_with_progress(&[from], to, &options, dir_handler)?;
|
||||||
} else {
|
} else {
|
||||||
let mut options = fs_extra::file::CopyOptions::default();
|
let mut options = fs_extra::file::CopyOptions::default();
|
||||||
if to.exists() {
|
if to.exists() {
|
||||||
match handle_replace(&msg_tx, from.clone(), to.clone()) {
|
match handle_replace(&msg_tx, from.clone(), to.clone(), false) {
|
||||||
ReplaceResult::Replace => {
|
ReplaceResult::Replace(_) => {
|
||||||
options.overwrite = true;
|
options.overwrite = true;
|
||||||
}
|
}
|
||||||
ReplaceResult::Skip => {
|
ReplaceResult::KeepBoth => {
|
||||||
|
match to.parent() {
|
||||||
|
Some(to_parent) => {
|
||||||
|
to = copy_unique_path(&from, &to_parent);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
log::warn!("failed to get parent of {:?}", to);
|
||||||
|
//TODO: error?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReplaceResult::Skip(_) => {
|
||||||
options.skip_exist = true;
|
options.skip_exist = true;
|
||||||
}
|
}
|
||||||
ReplaceResult::Cancel => {
|
ReplaceResult::Cancel => {
|
||||||
//TODO: be silent, but collect actual changes made for undo
|
//TODO: be silent, but collect actual changes made for undo
|
||||||
return Err(fs_extra::error::Error::new(
|
continue;
|
||||||
fs_extra::error::ErrorKind::Interrupted,
|
|
||||||
"operation cancelled",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue