Add Compress menu option to add files to zip archive
This commit is contained in:
parent
a85d51acac
commit
13cd7138cc
6 changed files with 268 additions and 29 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1271,6 +1271,7 @@ dependencies = [
|
|||
"url",
|
||||
"uzers",
|
||||
"vergen",
|
||||
"walkdir",
|
||||
"xdg",
|
||||
"xdg-mime",
|
||||
"zip",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ tar = "0.4.41"
|
|||
tokio = { version = "1", features = ["sync"] }
|
||||
trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "delete-info" }
|
||||
url = "2.5"
|
||||
walkdir = "2.5.0"
|
||||
xdg = { version = "2.5.2", optional = true }
|
||||
xdg-mime = "0.3"
|
||||
# Internationalization
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ size = Size
|
|||
|
||||
# Dialogs
|
||||
|
||||
## Compress Dialog
|
||||
create-archive = Create archive
|
||||
|
||||
## Empty Trash Dialog
|
||||
empty-trash = Empty trash
|
||||
empty-trash-warning = Are you sure you want to permanently delete all the items in Trash?
|
||||
|
|
@ -32,6 +35,7 @@ name-no-slashes = Name cannot contain slashes.
|
|||
|
||||
## Open/Save Dialog
|
||||
cancel = Cancel
|
||||
create = Create
|
||||
open = Open
|
||||
open-file = Open file
|
||||
open-folder = Open folder
|
||||
|
|
@ -79,6 +83,14 @@ no-history = No items in history.
|
|||
pending = Pending
|
||||
failed = Failed
|
||||
complete = Complete
|
||||
compressing = Compressing {$items} {$items ->
|
||||
[one] item
|
||||
*[other] items
|
||||
} from {$from} to {$to}
|
||||
compressed = Compressed {$items} {$items ->
|
||||
[one] item
|
||||
*[other] items
|
||||
} from {$from} to {$to}
|
||||
copy_noun = Copy
|
||||
creating = Creating {$name} in {$parent}
|
||||
created = Created {$name} in {$parent}
|
||||
|
|
@ -148,8 +160,9 @@ dark = Dark
|
|||
light = Light
|
||||
|
||||
# Context menu
|
||||
extract-here = Extract
|
||||
add-to-sidebar = Add to sidebar
|
||||
compress = Compress
|
||||
extract-here = Extract
|
||||
new-file = New file...
|
||||
new-folder = New folder...
|
||||
open-in-terminal = Open in terminal
|
||||
|
|
|
|||
122
src/app.rs
122
src/app.rs
|
|
@ -67,6 +67,7 @@ pub struct Flags {
|
|||
pub enum Action {
|
||||
About,
|
||||
AddToSidebar,
|
||||
Compress,
|
||||
Copy,
|
||||
Cut,
|
||||
EditHistory,
|
||||
|
|
@ -116,6 +117,7 @@ impl Action {
|
|||
match self {
|
||||
Action::About => Message::ToggleContextPage(ContextPage::About),
|
||||
Action::AddToSidebar => Message::AddToSidebar(entity_opt),
|
||||
Action::Compress => Message::Compress(entity_opt),
|
||||
Action::Copy => Message::Copy(entity_opt),
|
||||
Action::Cut => Message::Cut(entity_opt),
|
||||
Action::EditHistory => Message::ToggleContextPage(ContextPage::EditHistory),
|
||||
|
|
@ -210,6 +212,7 @@ pub enum Message {
|
|||
AddToSidebar(Option<Entity>),
|
||||
AppTheme(AppTheme),
|
||||
CloseToast(widget::ToastId),
|
||||
Compress(Option<Entity>),
|
||||
Config(Config),
|
||||
Copy(Option<Entity>),
|
||||
Cut(Option<Entity>),
|
||||
|
|
@ -297,8 +300,27 @@ impl ContextPage {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum ArchiveType {
|
||||
Zip,
|
||||
}
|
||||
|
||||
impl ArchiveType {
|
||||
pub fn extension(&self) -> String {
|
||||
match self {
|
||||
ArchiveType::Zip => ".zip".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DialogPage {
|
||||
Compress {
|
||||
paths: Vec<PathBuf>,
|
||||
to: PathBuf,
|
||||
name: String,
|
||||
archive_type: ArchiveType,
|
||||
},
|
||||
EmptyTrash,
|
||||
FailedOperation(u64),
|
||||
NewItem {
|
||||
|
|
@ -1264,6 +1286,23 @@ impl Application for App {
|
|||
config_set!(app_theme, app_theme);
|
||||
return self.update_config();
|
||||
}
|
||||
Message::Compress(entity_opt) => {
|
||||
let paths = self.selected_paths(entity_opt);
|
||||
if let Some(current_path) = paths.first() {
|
||||
if let Some(destination) = current_path.parent().zip(current_path.file_stem()) {
|
||||
let to = destination.0.to_path_buf();
|
||||
let name = destination.1.to_str().unwrap_or_default().to_string();
|
||||
let archive_type = ArchiveType::Zip;
|
||||
self.dialog_pages.push_back(DialogPage::Compress {
|
||||
paths,
|
||||
to,
|
||||
name,
|
||||
archive_type,
|
||||
});
|
||||
return widget::text_input::focus(self.dialog_text_input.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Config(config) => {
|
||||
if config != self.config {
|
||||
log::info!("update config");
|
||||
|
|
@ -1291,6 +1330,21 @@ impl Application for App {
|
|||
Message::DialogComplete => {
|
||||
if let Some(dialog_page) = self.dialog_pages.pop_front() {
|
||||
match dialog_page {
|
||||
DialogPage::Compress {
|
||||
paths,
|
||||
to,
|
||||
name,
|
||||
archive_type,
|
||||
} => {
|
||||
let extension = archive_type.extension();
|
||||
let name = format!("{}{}", name, extension);
|
||||
let to = to.join(name);
|
||||
self.operation(Operation::Compress {
|
||||
paths,
|
||||
to,
|
||||
archive_type,
|
||||
})
|
||||
}
|
||||
DialogPage::EmptyTrash => {
|
||||
self.operation(Operation::EmptyTrash);
|
||||
}
|
||||
|
|
@ -2323,6 +2377,74 @@ impl Application for App {
|
|||
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||
|
||||
let dialog = match dialog_page {
|
||||
DialogPage::Compress {
|
||||
paths,
|
||||
to,
|
||||
name,
|
||||
archive_type,
|
||||
} => {
|
||||
let mut dialog = widget::dialog(fl!("create-archive"));
|
||||
|
||||
let complete_maybe = if name.is_empty() {
|
||||
None
|
||||
} else if name == "." || name == ".." {
|
||||
dialog = dialog.tertiary_action(widget::text::body(fl!(
|
||||
"name-invalid",
|
||||
filename = name.as_str()
|
||||
)));
|
||||
None
|
||||
} else if name.contains('/') {
|
||||
dialog = dialog.tertiary_action(widget::text::body(fl!("name-no-slashes")));
|
||||
None
|
||||
} else {
|
||||
let extension = archive_type.extension();
|
||||
let name = format!("{}{}", name, extension);
|
||||
let path = to.join(&name);
|
||||
if path.exists() {
|
||||
dialog =
|
||||
dialog.tertiary_action(widget::text::body(fl!("file-already-exists")));
|
||||
None
|
||||
} else {
|
||||
if name.starts_with('.') {
|
||||
dialog = dialog.tertiary_action(widget::text::body(fl!("name-hidden")));
|
||||
}
|
||||
Some(Message::DialogComplete)
|
||||
}
|
||||
};
|
||||
|
||||
dialog
|
||||
.primary_action(
|
||||
widget::button::suggested(fl!("create"))
|
||||
.on_press_maybe(complete_maybe.clone()),
|
||||
)
|
||||
.secondary_action(
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
||||
)
|
||||
.control(
|
||||
widget::column::with_children(vec![
|
||||
widget::text::body(fl!("file-name")).into(),
|
||||
widget::row::with_children(vec![
|
||||
widget::text_input("", name.as_str())
|
||||
.id(self.dialog_text_input.clone())
|
||||
.on_input(move |name| {
|
||||
Message::DialogUpdate(DialogPage::Compress {
|
||||
paths: paths.clone(),
|
||||
to: to.clone(),
|
||||
name: name.clone(),
|
||||
archive_type: archive_type.clone(),
|
||||
})
|
||||
})
|
||||
.on_submit_maybe(complete_maybe)
|
||||
.into(),
|
||||
widget::text::body(".zip").into(),
|
||||
])
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(space_xxs)
|
||||
.into(),
|
||||
])
|
||||
.spacing(space_xxs),
|
||||
)
|
||||
}
|
||||
DialogPage::EmptyTrash => widget::dialog(fl!("empty-trash"))
|
||||
.body(fl!("empty-trash-warning"))
|
||||
.primary_action(
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ pub fn context_menu<'a>(
|
|||
children.push(menu_item(fl!("cut"), Action::Cut).into());
|
||||
children.push(menu_item(fl!("copy"), Action::Copy).into());
|
||||
|
||||
children.push(container(horizontal_rule(1)).padding([0, 8]).into());
|
||||
let supported_archive_types = ["application/x-tar", "application/zip"]
|
||||
.iter()
|
||||
.filter_map(|mime_type| mime_type.parse::<Mime>().ok())
|
||||
|
|
@ -133,6 +134,8 @@ pub fn context_menu<'a>(
|
|||
if selected_types.is_empty() {
|
||||
children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into());
|
||||
}
|
||||
children.push(menu_item(fl!("compress"), Action::Compress).into());
|
||||
children.push(container(horizontal_rule(1)).padding([0, 8]).into());
|
||||
|
||||
//TODO: Print?
|
||||
children.push(container(horizontal_rule(1)).padding([0, 8]).into());
|
||||
|
|
|
|||
155
src/operation.rs
155
src/operation.rs
|
|
@ -1,20 +1,19 @@
|
|||
use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt};
|
||||
use mime_guess::MimeGuess;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs,
|
||||
io::{self, Error},
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
sync::{
|
||||
atomic::{self, AtomicU64},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::{
|
||||
app::{DialogPage, Message},
|
||||
app::{ArchiveType, DialogPage, Message},
|
||||
config::IconSizes,
|
||||
fl, tab,
|
||||
};
|
||||
|
|
@ -130,6 +129,12 @@ impl From<ReplaceResult> for fs_extra::dir::TransitProcessResult {
|
|||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum Operation {
|
||||
/// Compress files
|
||||
Compress {
|
||||
paths: Vec<PathBuf>,
|
||||
to: PathBuf,
|
||||
archive_type: ArchiveType,
|
||||
},
|
||||
/// Copy items
|
||||
Copy {
|
||||
paths: Vec<PathBuf>,
|
||||
|
|
@ -241,6 +246,12 @@ fn paths_parent_name<'a>(paths: &'a Vec<PathBuf>) -> Cow<'a, str> {
|
|||
impl Operation {
|
||||
pub fn pending_text(&self) -> String {
|
||||
match self {
|
||||
Self::Compress { paths, to, .. } => fl!(
|
||||
"compressing",
|
||||
items = paths.len(),
|
||||
from = paths_parent_name(paths),
|
||||
to = file_name(to)
|
||||
),
|
||||
Self::Copy { paths, to } => fl!(
|
||||
"copying",
|
||||
items = paths.len(),
|
||||
|
|
@ -285,6 +296,12 @@ impl Operation {
|
|||
|
||||
pub fn completed_text(&self) -> String {
|
||||
match self {
|
||||
Self::Compress { paths, to, .. } => fl!(
|
||||
"compressed",
|
||||
items = paths.len(),
|
||||
from = paths_parent_name(paths),
|
||||
to = file_name(to)
|
||||
),
|
||||
Self::Copy { paths, to } => fl!(
|
||||
"copied",
|
||||
items = paths.len(),
|
||||
|
|
@ -327,7 +344,9 @@ impl Operation {
|
|||
|
||||
pub fn toast(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Compress { .. } => Some(self.completed_text()),
|
||||
Self::Delete { .. } => Some(self.completed_text()),
|
||||
Self::Extract { .. } => Some(self.completed_text()),
|
||||
//TODO: more toasts
|
||||
_ => None,
|
||||
}
|
||||
|
|
@ -348,6 +367,79 @@ impl Operation {
|
|||
//TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE
|
||||
//TODO: SAFELY HANDLE CANCEL
|
||||
match self {
|
||||
Self::Compress {
|
||||
paths,
|
||||
to,
|
||||
archive_type,
|
||||
} => {
|
||||
let msg_tx = msg_tx.clone();
|
||||
tokio::task::spawn_blocking(move || -> Result<(), String> {
|
||||
match archive_type {
|
||||
ArchiveType::Zip => {
|
||||
let mut archive = fs::File::create(&to)
|
||||
.map(io::BufWriter::new)
|
||||
.map(zip::ZipWriter::new)
|
||||
.map_err(err_str)?;
|
||||
|
||||
let zip_options = zip::write::SimpleFileOptions::default();
|
||||
|
||||
let mut paths = paths;
|
||||
for path in paths.clone().iter() {
|
||||
if path.is_dir() {
|
||||
let new_paths_it = WalkDir::new(path).into_iter();
|
||||
for entry in new_paths_it.skip(1) {
|
||||
let entry = entry.map_err(err_str)?;
|
||||
paths.push(entry.path().to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total_paths = paths.len();
|
||||
for (i, path) in paths.iter().enumerate() {
|
||||
executor::block_on(async {
|
||||
let total_progress = (i as f32) / total_paths as f32;
|
||||
let _ = msg_tx
|
||||
.lock()
|
||||
.await
|
||||
.send(Message::PendingProgress(id, 100.0 * total_progress))
|
||||
.await;
|
||||
});
|
||||
|
||||
if let Some(relative_root) = to.parent() {
|
||||
if let Some(relative_path) =
|
||||
path.strip_prefix(relative_root).map_err(err_str)?.to_str()
|
||||
{
|
||||
if path.is_file() {
|
||||
archive
|
||||
.start_file(relative_path, zip_options)
|
||||
.map_err(err_str)?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let mut file = fs::File::open(&path)
|
||||
.map(io::BufReader::new)
|
||||
.map_err(err_str)?;
|
||||
|
||||
file.read_to_end(&mut buffer).map_err(err_str)?;
|
||||
archive.write_all(&buffer).map_err(err_str)?;
|
||||
} else {
|
||||
archive
|
||||
.add_directory(relative_path, zip_options)
|
||||
.map_err(err_str)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
archive.finish().map_err(err_str)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map_err(err_str)?;
|
||||
}
|
||||
Self::Copy { paths, to } => {
|
||||
// Handle duplicate file names by renaming paths
|
||||
let (paths, to): (Vec<_>, Vec<_>) = tokio::task::spawn_blocking(move || {
|
||||
|
|
@ -494,10 +586,21 @@ impl Operation {
|
|||
.await;
|
||||
}
|
||||
Self::Extract { paths, to } => {
|
||||
for path in paths {
|
||||
let to = to.to_owned();
|
||||
let msg_tx = msg_tx.clone();
|
||||
tokio::task::spawn_blocking(move || -> Result<(), String> {
|
||||
let total_paths = paths.len();
|
||||
for (i, path) in paths.iter().enumerate() {
|
||||
executor::block_on(async {
|
||||
let total_progress = (i as f32) / total_paths as f32;
|
||||
let _ = msg_tx
|
||||
.lock()
|
||||
.await
|
||||
.send(Message::PendingProgress(id, 100.0 * total_progress))
|
||||
.await;
|
||||
});
|
||||
|
||||
let to = to.to_owned();
|
||||
|
||||
tokio::task::spawn_blocking(move || -> Result<(), String> {
|
||||
if let Some(file_stem) = path.file_stem() {
|
||||
let mut new_dir = to.join(file_stem);
|
||||
if new_dir.exists() {
|
||||
|
|
@ -510,32 +613,28 @@ impl Operation {
|
|||
|
||||
if let Some(mime) = mime_guess::from_path(&path).first() {
|
||||
match mime.essence_str() {
|
||||
"application/x-tar" => {
|
||||
return fs::File::open(path)
|
||||
.map(io::BufReader::new)
|
||||
.map(tar::Archive::new)
|
||||
.and_then(|mut archive| archive.unpack(new_dir))
|
||||
.map_err(err_str)
|
||||
}
|
||||
"application/zip" => {
|
||||
return fs::File::open(path)
|
||||
.map(io::BufReader::new)
|
||||
.map(zip::ZipArchive::new)
|
||||
.map_err(err_str)?
|
||||
.and_then(|mut archive| archive.extract(new_dir))
|
||||
.map_err(err_str)
|
||||
}
|
||||
"application/x-tar" => fs::File::open(path)
|
||||
.map(io::BufReader::new)
|
||||
.map(tar::Archive::new)
|
||||
.and_then(|mut archive| archive.unpack(new_dir))
|
||||
.map_err(err_str)?,
|
||||
"application/zip" => fs::File::open(path)
|
||||
.map(io::BufReader::new)
|
||||
.map(zip::ZipArchive::new)
|
||||
.map_err(err_str)?
|
||||
.and_then(|mut archive| archive.extract(new_dir))
|
||||
.map_err(err_str)?,
|
||||
_ => Err(format!("unsupported mime type {:?}", mime))?,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map_err(err_str)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map_err(err_str)?;
|
||||
}
|
||||
Self::Move { paths, to } => {
|
||||
let msg_tx = msg_tx.clone();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue