Add dialog for move conflict, part of #180

This commit is contained in:
Jeremy Soller 2024-07-08 11:51:07 -06:00
parent f075d07bfd
commit 35fcb011c5
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
6 changed files with 305 additions and 147 deletions

View file

@ -40,6 +40,7 @@ use std::{
sync::Arc,
time::{self, Instant},
};
use tokio::sync::mpsc;
use crate::localize::LANGUAGE_SORTER;
use crate::tab::HOVER_DURATION;
@ -208,6 +209,7 @@ pub enum Message {
Cut(Option<Entity>),
DialogCancel,
DialogComplete,
DialogPush(DialogPage),
DialogUpdate(DialogPage),
EditLocation(Option<Entity>),
Key(Modifiers, Key),
@ -280,7 +282,7 @@ impl ContextPage {
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug)]
pub enum DialogPage {
EmptyTrash,
FailedOperation(u64),
@ -295,6 +297,11 @@ pub enum DialogPage {
name: String,
dir: bool,
},
Replace {
from: tab::Item,
to: tab::Item,
tx: mpsc::Sender<fs_extra::dir::TransitProcessResult>,
},
}
pub struct FavoriteIndex(usize);
@ -1197,12 +1204,27 @@ impl Application for App {
let to = parent.join(name);
self.operation(Operation::Rename { from, to });
}
DialogPage::Replace { tx, .. } => {
return Command::perform(
async move {
let _ = tx
.send(fs_extra::dir::TransitProcessResult::Overwrite)
.await;
message::none()
},
|x| x,
);
}
}
}
}
Message::DialogPush(dialog_page) => {
self.dialog_pages.push_back(dialog_page);
}
Message::DialogUpdate(dialog_page) => {
//TODO: panicless way to do this?
self.dialog_pages[0] = dialog_page;
if !self.dialog_pages.is_empty() {
self.dialog_pages[0] = dialog_page;
}
}
Message::EditLocation(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
@ -2226,6 +2248,18 @@ impl Application for App {
.spacing(space_xxs),
)
}
DialogPage::Replace { from, to, .. } => {
widget::dialog(fl!("replace-title", filename = to.name.as_str()))
.body(fl!("replace-warning-operation"))
.control(to.replace_view(fl!("original-file"), IconSizes::default()))
.control(from.replace_view(fl!("replace-with"), IconSizes::default()))
.primary_action(
widget::button::suggested(fl!("replace")).on_press(Message::DialogComplete),
)
.secondary_action(
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
)
}
};
Some(dialog.into())

View file

@ -1,4 +1,4 @@
use cosmic::iced::futures::{channel::mpsc, executor, SinkExt};
use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt};
use std::{
fs,
path::PathBuf,
@ -7,8 +7,13 @@ use std::{
Arc,
},
};
use tokio::sync::{mpsc, Mutex};
use crate::{app::Message, fl};
use crate::{
app::{DialogPage, Message},
config::IconSizes,
fl, tab,
};
fn err_str<T: ToString>(err: T) -> String {
err.to_string()
@ -53,7 +58,7 @@ impl Operation {
pub async fn perform(
self,
id: u64,
msg_tx: &Arc<tokio::sync::Mutex<mpsc::Sender<Message>>>,
msg_tx: &Arc<Mutex<Sender<Message>>>,
) -> Result<(), String> {
let _ = msg_tx
.lock()
@ -247,8 +252,59 @@ impl Operation {
))
.await;
});
//TODO: handle exceptions
fs_extra::dir::TransitProcessResult::ContinueOrAbort
match progress.state {
fs_extra::dir::TransitState::Normal => {
fs_extra::dir::TransitProcessResult::ContinueOrAbort
}
fs_extra::dir::TransitState::Exists => {
let Some(file_from) = progress.file_from.clone() else {
log::warn!("missing file_from in progress");
return fs_extra::dir::TransitProcessResult::Abort;
};
let item_from =
match tab::item_from_path(file_from, IconSizes::default()) {
Ok(ok) => ok,
Err(err) => {
log::warn!("{}", err);
return fs_extra::dir::TransitProcessResult::Abort;
}
};
let Some(file_to) = progress.file_to.clone() else {
log::warn!("missing file_to in progress");
return fs_extra::dir::TransitProcessResult::Abort;
};
let item_to =
match tab::item_from_path(file_to, IconSizes::default()) {
Ok(ok) => ok,
Err(err) => {
log::warn!("{}", err);
return fs_extra::dir::TransitProcessResult::Abort;
}
};
executor::block_on(async {
let (tx, mut rx) = mpsc::channel(1);
let _ = msg_tx
.lock()
.await
.send(Message::DialogPush(DialogPage::Replace {
from: item_from,
to: item_to,
tx,
}))
.await;
rx.recv()
.await
.unwrap_or(fs_extra::dir::TransitProcessResult::Abort)
})
}
fs_extra::dir::TransitState::NoAccess => {
//TODO: permission error dialog
fs_extra::dir::TransitProcessResult::ContinueOrAbort
}
}
})
})
.await

View file

@ -269,6 +269,25 @@ pub fn item_from_entry(
}
}
pub fn item_from_path<P: Into<PathBuf>>(path: P, sizes: IconSizes) -> Result<Item, String> {
let path = path.into();
let name_os = path
.file_name()
.ok_or_else(|| format!("failed to get file name from path {:?}", path))?;
let name = name_os
.to_str()
.ok_or_else(|| {
format!(
"failed to parse file name for {:?}: {:?} is not valid UTF-8",
path, name_os
)
})?
.to_string();
let metadata = fs::metadata(&path)
.map_err(|err| format!("failed to read metadata for {:?}: {}", path, err))?;
Ok(item_from_entry(path, name, metadata, sizes))
}
pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
let mut items = Vec::new();
match fs::read_dir(tab_path) {
@ -788,6 +807,47 @@ impl Item {
column.into()
}
pub fn replace_view(
&self,
heading: String,
sizes: IconSizes,
) -> Element<'static, app::Message> {
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
let mut row = widget::row().spacing(space_xxxs);
row = row.push(self.preview(sizes));
let mut column = widget::column().spacing(space_xxxs);
column = column.push(widget::text::heading(heading));
//TODO: translate!
//TODO: correct display of folder size?
match &self.metadata {
ItemMetadata::Path { metadata, children } => {
if metadata.is_dir() {
column = column.push(widget::text(format!("Items: {}", children)));
} else {
column = column.push(widget::text(format!(
"Size: {}",
format_size(metadata.len())
)));
}
if let Ok(time) = metadata.modified() {
column = column.push(widget::text(format!(
"Last modified: {}",
chrono::DateTime::<chrono::Local>::from(time).format(TIME_FORMAT)
)));
}
}
ItemMetadata::Trash { .. } => {
//TODO: trash metadata
}
}
row = row.push(column);
row.into()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]