use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::runtime::{Command, Confirm, Task, TaskEvent, TaskId, TaskOutcome};
use crate::widgets::{Toast, TreeDrop};
use super::details::{FileDetails, PAGE};
use super::ops::{self, FileChange, FileError, NameProblem, is_within, name_of, parent_key};
use super::watch::Live;
pub const ROOT: &str = "";
#[must_use]
pub fn child_key(parent: &str, name: &str) -> String {
if parent.is_empty() { name.to_owned() } else { format!("{parent}/{name}") }
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FolderEntry {
pub name: String,
pub folder: bool,
}
impl FolderEntry {
pub(super) fn list(path: &Path) -> Result<Vec<Self>, FileError> {
let mut entries = Vec::new();
for entry in std::fs::read_dir(path).map_err(ops::read_error)? {
let entry = entry.map_err(ops::read_error)?;
let name = entry.file_name().to_string_lossy().into_owned();
let folder = entry.file_type().is_ok_and(|kind| kind.is_dir());
entries.push(Self { name, folder });
}
entries.sort_by(|a, b| b.folder.cmp(&a.folder).then_with(|| a.name.cmp(&b.name)));
Ok(entries)
}
#[must_use]
pub fn is_hidden(&self) -> bool {
self.name.starts_with('.')
}
pub fn read_folder(path: &Path) -> Result<Vec<Self>, String> {
let mut entries = Vec::new();
for entry in std::fs::read_dir(path).map_err(|error| error.to_string())? {
let entry = entry.map_err(|error| error.to_string())?;
let name = entry.file_name().to_string_lossy().into_owned();
let folder = entry.file_type().is_ok_and(|kind| kind.is_dir());
entries.push(Self { name, folder });
}
entries.sort_by(|a, b| b.folder.cmp(&a.folder).then_with(|| a.name.cmp(&b.name)));
Ok(entries)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NameFor {
File,
Folder,
Rename(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Naming {
pub purpose: NameFor,
pub folder: String,
pub value: String,
pub tried: bool,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum FileManagerMsg {
Select(String),
Choose(Vec<String>),
Expand(String, bool),
Read(String, Result<Vec<FolderEntry>, String>),
Listed(String, Result<Vec<FolderEntry>, FileError>),
NewFile(String),
NewFolder(String),
Rename(String),
Cut(String),
Copy(String),
Paste(String),
DropCut,
Drop(TreeDrop),
Delete(String),
DeleteConfirmed(Vec<String>),
Trash(String),
ShowHidden(bool),
Work(TaskEvent),
Stop,
Refresh,
Name(String),
Submit,
CloseNaming,
Done(Vec<(String, Result<FileChange, FileError>)>),
Changed(u64, Vec<crate::storage::FolderChange>),
Detail(Vec<String>),
Detailed(Vec<(String, Option<FileDetails>)>),
Enter(String),
Leave,
}
#[derive(Debug, Clone)]
pub struct FileWork {
id: TaskId,
entries: usize,
done: f32,
note: String,
}
impl FileWork {
#[must_use]
pub fn id(&self) -> TaskId {
self.id
}
#[must_use]
pub fn done(&self) -> f32 {
self.done
}
#[must_use]
pub fn note(&self) -> &str {
&self.note
}
#[must_use]
pub fn entries(&self) -> usize {
self.entries
}
}
#[derive(Debug, Default)]
enum Trash {
#[default]
Off,
Home,
In(PathBuf),
}
impl Trash {
fn folder(&self) -> Option<PathBuf> {
match self {
Self::Off => None,
Self::Home => super::trash::home_trash(),
Self::In(folder) => Some(folder.clone()),
}
}
}
pub(super) type Wrap<Msg> = Arc<dyn Fn(FileManagerMsg) -> Msg + Send + Sync>;
#[derive(Debug)]
pub struct FileManagerState {
root: PathBuf,
confined: bool,
following: bool,
shown: String,
children: BTreeMap<String, Vec<FolderEntry>>,
details: BTreeMap<String, Option<FileDetails>>,
reading: BTreeSet<String>,
open: BTreeSet<String>,
loading: BTreeSet<String>,
selected: Option<String>,
chosen: Vec<String>,
errors: BTreeMap<String, String>,
cut: Vec<String>,
copying: bool,
trash: Trash,
hidden: bool,
work: Option<FileWork>,
naming: Option<Naming>,
pub(super) live: Live,
pub(super) runs: u64,
}
impl FileManagerState {
pub const ROOT: &'static str = ROOT;
#[must_use]
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
confined: false,
following: false,
shown: ROOT.to_owned(),
children: BTreeMap::new(),
details: BTreeMap::new(),
reading: BTreeSet::new(),
open: BTreeSet::from([ROOT.to_owned()]),
loading: BTreeSet::new(),
selected: None,
chosen: Vec::new(),
errors: BTreeMap::new(),
cut: Vec::new(),
copying: false,
trash: Trash::Off,
hidden: false,
work: None,
naming: None,
live: Live::Off,
runs: 0,
}
}
#[must_use]
pub fn confined(mut self) -> Self {
self.confined = true;
self
}
#[must_use]
pub fn is_confined(&self) -> bool {
self.confined
}
#[must_use]
pub fn trashing(mut self) -> Self {
self.trash = Trash::Home;
self
}
#[must_use]
pub fn trashing_in(mut self, folder: impl Into<PathBuf>) -> Self {
self.trash = Trash::In(folder.into());
self
}
#[must_use]
pub fn is_trashing(&self) -> bool {
!matches!(self.trash, Trash::Off)
}
#[must_use]
pub fn showing_hidden(mut self, showing: bool) -> Self {
self.hidden = showing;
self
}
pub fn set_showing_hidden(&mut self, showing: bool) {
self.hidden = showing;
}
#[must_use]
pub fn shows_hidden(&self) -> bool {
self.hidden
}
#[must_use]
pub fn following(mut self, following: bool) -> Self {
self.following = following;
self
}
pub fn set_following(&mut self, following: bool) {
self.following = following;
if !following {
self.live = Live::Off;
}
}
#[must_use]
pub fn follows_changes(&self) -> bool {
self.following
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub fn children(&self, key: &str) -> Option<&[FolderEntry]> {
self.children.get(key).map(Vec::as_slice)
}
#[must_use]
pub fn folder(&self) -> &str {
&self.shown
}
#[must_use]
pub fn details(&self, key: &str) -> Option<Option<&FileDetails>> {
self.details.get(key).map(Option::as_ref)
}
#[must_use]
pub fn has_details(&self, key: &str) -> bool {
self.details.contains_key(key) || self.reading.contains(key)
}
#[must_use]
pub fn is_open(&self, key: &str) -> bool {
self.open.contains(key)
}
#[must_use]
pub fn is_loading(&self, key: &str) -> bool {
self.loading.contains(key)
}
#[must_use]
pub fn selected(&self) -> Option<&str> {
self.selected.as_deref()
}
#[must_use]
pub fn chosen(&self) -> &[String] {
&self.chosen
}
#[must_use]
pub fn error(&self) -> Option<&str> {
self.folder_error(ROOT)
}
#[must_use]
pub fn folder_error(&self, key: &str) -> Option<&str> {
self.errors.get(key).map(String::as_str)
}
#[must_use]
pub fn cut(&self) -> &[String] {
if self.copying { &[] } else { &self.cut }
}
#[must_use]
pub fn copied(&self) -> &[String] {
if self.copying { &self.cut } else { &[] }
}
#[must_use]
pub fn pending(&self) -> &[String] {
&self.cut
}
#[must_use]
pub fn is_copying(&self) -> bool {
self.copying
}
#[must_use]
pub fn is_cut(&self, key: &str) -> bool {
!self.copying && self.cut.iter().any(|cut| is_within(key, cut))
}
#[must_use]
pub fn shown_children(&self, key: &str) -> Option<Vec<&FolderEntry>> {
let entries = self.children(key)?;
Some(entries.iter().filter(|entry| self.hidden || !entry.is_hidden()).collect())
}
#[must_use]
pub fn work(&self) -> Option<&FileWork> {
self.work.as_ref()
}
#[must_use]
pub fn naming(&self) -> Option<&Naming> {
self.naming.as_ref()
}
#[must_use]
pub fn is_folder(&self, key: &str) -> bool {
let name = name_of(key);
self.children(parent_key(key)).is_some_and(|entries| entries.iter().any(|e| e.folder && e.name == name))
}
#[must_use]
pub fn folder_keys(&self) -> BTreeSet<String> {
self.children
.iter()
.flat_map(|(parent, entries)| {
entries.iter().filter(|entry| entry.folder).map(move |entry| child_key(parent, &entry.name))
})
.collect()
}
#[must_use]
pub fn path(&self, key: &str) -> PathBuf {
key.split('/').filter(|part| !part.is_empty()).fold(self.root.clone(), |path, part| path.join(part))
}
pub fn select(&mut self, key: &str) {
self.selected = Some(key.to_owned());
self.chosen = vec![key.to_owned()];
}
#[must_use]
pub fn targets(&self, key: &str) -> Vec<String> {
targets_of(&self.chosen, key)
}
#[must_use]
pub fn naming_problem(&self) -> Option<NameProblem> {
let naming = self.naming.as_ref()?;
let siblings = self.children(&naming.folder).unwrap_or_default().iter().map(|entry| entry.name.as_str());
let current = match &naming.purpose {
NameFor::Rename(key) => Some(name_of(key)),
NameFor::File | NameFor::Folder => None,
};
let problem = ops::check_name(&naming.value, siblings, current).err()?;
(problem != NameProblem::Empty || naming.tried).then_some(problem)
}
#[must_use]
pub fn visible_folders(&self) -> Vec<String> {
self.open
.iter()
.filter(|key| {
let mut above = key.as_str();
while above != ROOT {
above = parent_key(above);
if !self.open.contains(above) {
return false;
}
}
true
})
.cloned()
.collect()
}
fn shown_folders(&self) -> Vec<String> {
std::iter::once(ROOT.to_owned())
.chain(self.open.iter().filter(|key| *key != ROOT && self.children.contains_key(*key)).cloned())
.collect()
}
fn expand(&mut self, key: &str, open: bool) -> bool {
if !open {
self.open.remove(key);
return false;
}
self.open.insert(key.to_owned());
if self.children.contains_key(key) || self.loading.contains(key) {
return false;
}
self.loading.insert(key.to_owned());
true
}
fn take_read(&mut self, key: &str, entries: Result<Vec<FolderEntry>, String>) {
self.loading.remove(key);
if key != ROOT && !self.open.contains(key) {
return;
}
match entries {
Ok(entries) => {
self.errors.remove(key);
self.children.insert(key.to_owned(), entries);
self.details.retain(|entry, _| parent_key(entry) != key);
self.reading.retain(|entry| parent_key(entry) != key);
self.prune(key);
}
Err(problem) => {
self.errors.insert(key.to_owned(), problem);
if key != ROOT {
self.children.insert(key.to_owned(), Vec::new());
}
}
}
}
fn prune(&mut self, key: &str) {
let Some(entries) = self.children.get(key) else { return };
let names: BTreeSet<&str> = entries.iter().map(|entry| entry.name.as_str()).collect();
let held = self
.open
.iter()
.chain(self.children.keys())
.chain(&self.loading)
.chain(&self.cut)
.chain(&self.chosen)
.chain(&self.selected);
let gone: BTreeSet<String> = held
.filter(|held| *held != key && (key == ROOT || is_within(held, key)))
.map(|held| {
let below = if key == ROOT { held.as_str() } else { &held[key.len() + 1..] };
below.split('/').next().unwrap_or(below)
})
.filter(|name| !names.contains(name))
.map(|name| child_key(key, name))
.collect();
for child in gone {
self.forget(&child);
}
}
fn rekey(&mut self, from: &str, to: &str) {
let moved = |key: &str| is_within(key, from).then(|| format!("{to}{}", &key[from.len()..]));
self.open = self.open.iter().map(|key| moved(key).unwrap_or_else(|| key.clone())).collect();
self.loading.retain(|key| !is_within(key, from));
self.children = std::mem::take(&mut self.children)
.into_iter()
.map(|(key, entries)| (moved(&key).unwrap_or(key), entries))
.collect();
self.details = std::mem::take(&mut self.details)
.into_iter()
.map(|(key, details)| (moved(&key).unwrap_or(key), details))
.collect();
self.errors = std::mem::take(&mut self.errors)
.into_iter()
.map(|(key, problem)| (moved(&key).unwrap_or(key), problem))
.collect();
self.reading.retain(|key| !is_within(key, from));
if let Some(new) = moved(&self.shown) {
self.shown = new;
}
for key in self.selected.iter_mut().chain(&mut self.cut).chain(&mut self.chosen) {
if let Some(new) = moved(key) {
*key = new;
}
}
}
fn forget(&mut self, key: &str) {
if is_within(&self.shown, key) {
self.shown = parent_key(key).to_owned();
}
self.details.retain(|entry, _| !is_within(entry, key));
self.reading.retain(|entry| !is_within(entry, key));
self.open.retain(|open| !is_within(open, key));
self.loading.retain(|loading| !is_within(loading, key));
self.children.retain(|folder, _| !is_within(folder, key));
self.errors.retain(|folder, _| !is_within(folder, key));
self.cut.retain(|cut| !is_within(cut, key));
self.chosen.retain(|chosen| !is_within(chosen, key));
if self.selected.as_deref().is_some_and(|selected| is_within(selected, key)) {
let parent = parent_key(key);
self.selected = (parent != ROOT).then(|| parent.to_owned());
}
}
}
pub(super) fn targets_of(chosen: &[String], key: &str) -> Vec<String> {
let keys = if chosen.len() > 1 && chosen.iter().any(|selected| selected == key) {
chosen.to_vec()
} else {
vec![key.to_owned()]
};
outermost(keys)
}
pub(super) fn outermost(keys: Vec<String>) -> Vec<String> {
let all = keys.clone();
keys.into_iter()
.filter(|key| key != ROOT && !all.iter().any(|other| other != key && is_within(key, other)))
.collect()
}
const NAMES_SHOWN: usize = 5;
impl FileManagerState {
pub fn load<Msg: Clone + Send + 'static>(
&mut self,
wrap: impl Fn(FileManagerMsg) -> Msg + Send + Sync + 'static,
) -> Command<Msg> {
let wrap: Wrap<Msg> = Arc::new(wrap);
self.with_wrap(&wrap, Self::load_with)
}
pub fn update<Msg: Clone + Send + 'static>(
&mut self,
message: FileManagerMsg,
wrap: impl Fn(FileManagerMsg) -> Msg + Send + Sync + 'static,
) -> Command<Msg> {
let wrap: Wrap<Msg> = Arc::new(wrap);
self.with_wrap(&wrap, |state, wrap| state.apply(message, wrap))
}
fn with_wrap<Msg: Clone + Send + 'static>(
&mut self,
wrap: &Wrap<Msg>,
work: impl FnOnce(&mut Self, &Wrap<Msg>) -> Command<Msg>,
) -> Command<Msg> {
let command = work(self, wrap);
let followed = super::watch::follow(self, wrap);
Command::batch([command, followed])
}
fn load_with<Msg: Clone + Send + 'static>(&mut self, wrap: &Wrap<Msg>) -> Command<Msg> {
if self.children.contains_key(ROOT) {
let shown = self.shown_folders();
return self.reread(shown, wrap);
}
if !self.loading.insert(ROOT.to_owned()) {
return Command::none();
}
self.read_folder(ROOT, wrap)
}
pub fn detail<Msg: Clone + Send + 'static>(
&mut self,
keys: Vec<String>,
wrap: impl Fn(FileManagerMsg) -> Msg + Send + Sync + 'static,
) -> Command<Msg> {
let wrap: Wrap<Msg> = Arc::new(wrap);
self.read_details(keys, &wrap)
}
pub fn detail_page<Msg: Clone + Send + 'static>(
&mut self,
folder: &str,
wrap: impl Fn(FileManagerMsg) -> Msg + Send + Sync + 'static,
) -> Command<Msg> {
let wrap: Wrap<Msg> = Arc::new(wrap);
self.read_page(folder, &wrap)
}
fn enter<Msg: Clone + Send + 'static>(&mut self, key: &str, wrap: &Wrap<Msg>) -> Command<Msg> {
if key != ROOT && !self.is_folder(key) {
return Command::none();
}
self.shown = key.to_owned();
self.selected = None;
self.chosen.clear();
let read = self.expand(key, true);
if read { self.read_folder(key, wrap) } else { Command::none() }
}
#[must_use]
pub fn detail_gaps(&self, folder: &str) -> Vec<String> {
let Some(entries) = self.shown_children(folder) else { return Vec::new() };
let keys: Vec<String> = entries.iter().map(|entry| child_key(folder, &entry.name)).collect();
let at = self.selected.as_deref().and_then(|cursor| keys.iter().position(|key| key == cursor)).unwrap_or(0);
let start = at.saturating_sub(PAGE / 2);
keys.into_iter().skip(start).take(PAGE).filter(|key| !self.has_details(key)).collect()
}
pub(super) fn read_page<Msg: Clone + Send + 'static>(&mut self, folder: &str, wrap: &Wrap<Msg>) -> Command<Msg> {
let page = self.detail_gaps(folder);
self.read_details(page, wrap)
}
pub(super) fn read_details<Msg: Clone + Send + 'static>(
&mut self,
keys: Vec<String>,
wrap: &Wrap<Msg>,
) -> Command<Msg> {
let wanted: Vec<String> = keys.into_iter().filter(|key| key != ROOT && !self.has_details(key)).collect();
if wanted.is_empty() {
return Command::none();
}
for key in &wanted {
self.reading.insert(key.clone());
}
let (root, wrap) = (self.root.clone(), Arc::clone(wrap));
Command::perform(move || {
let read = wanted
.iter()
.map(|key| {
let path =
key.split('/').filter(|part| !part.is_empty()).fold(root.clone(), |at, part| at.join(part));
(key.clone(), FileDetails::read(&path))
})
.collect();
wrap(FileManagerMsg::Detailed(read))
})
}
fn read_folder<Msg: Clone + Send + 'static>(&self, key: &str, wrap: &Wrap<Msg>) -> Command<Msg> {
let (key, path, wrap) = (key.to_owned(), self.path(key), Arc::clone(wrap));
Command::perform(move || wrap(FileManagerMsg::Listed(key.clone(), FolderEntry::list(&path))))
}
pub(super) fn reread<Msg: Clone + Send + 'static>(&mut self, keys: Vec<String>, wrap: &Wrap<Msg>) -> Command<Msg> {
let keys: Vec<String> = keys.into_iter().filter(|key| key == ROOT || self.is_open(key)).collect();
let commands: Vec<Command<Msg>> = keys
.into_iter()
.map(|key| {
self.loading.insert(key.clone());
self.read_folder(&key, wrap)
})
.collect();
Command::batch(commands)
}
pub(super) fn refresh<Msg: Clone + Send + 'static>(&mut self, wrap: &Wrap<Msg>) -> Command<Msg> {
let shown = self.shown_folders();
self.reread(shown, wrap)
}
fn apply<Msg: Clone + Send + 'static>(&mut self, message: FileManagerMsg, wrap: &Wrap<Msg>) -> Command<Msg> {
match message {
FileManagerMsg::Select(key) => {
self.selected = Some(key);
Command::none()
}
FileManagerMsg::Choose(keys) => {
self.chosen = keys;
Command::none()
}
FileManagerMsg::Expand(key, open) => {
if !self.expand(&key, open) {
return Command::none();
}
self.read_folder(&key, wrap)
}
FileManagerMsg::Read(key, entries) => {
self.take_read(&key, entries);
Command::none()
}
FileManagerMsg::Listed(key, entries) => {
self.take_read(&key, entries.map_err(|problem| problem.message()));
Command::none()
}
FileManagerMsg::NewFile(folder) => self.ask_name(NameFor::File, folder, String::new(), wrap),
FileManagerMsg::NewFolder(folder) => self.ask_name(NameFor::Folder, folder, String::new(), wrap),
FileManagerMsg::Rename(key) => {
let (folder, name) = (parent_key(&key).to_owned(), name_of(&key).to_owned());
self.ask_name(NameFor::Rename(key), folder, name, wrap)
}
FileManagerMsg::Cut(key) => {
self.cut = self.targets(&key);
self.copying = false;
Command::none()
}
FileManagerMsg::Copy(key) => {
self.cut = self.targets(&key);
self.copying = true;
Command::none()
}
FileManagerMsg::DropCut => {
self.cut.clear();
self.copying = false;
Command::none()
}
FileManagerMsg::Paste(into) => {
let keys = self.cut.clone();
if self.copying {
return self.copy_all(keys, into, wrap);
}
self.move_all(keys, into, wrap)
}
FileManagerMsg::Drop(TreeDrop { keys, into }) => {
self.move_all(outermost(keys), into.unwrap_or_default(), wrap)
}
FileManagerMsg::Delete(key) => self.ask_delete(self.targets(&key), wrap),
FileManagerMsg::DeleteConfirmed(keys) => {
let confined = self.confined;
self.run_each(keys, wrap, move |root, key| (key.to_owned(), ops::delete(root, key, confined)))
}
FileManagerMsg::Trash(key) => {
let keys = self.targets(&key);
self.trash_all(keys, wrap)
}
FileManagerMsg::ShowHidden(showing) => {
self.hidden = showing;
Command::none()
}
FileManagerMsg::Work(event) => self.took_event(event, wrap),
FileManagerMsg::Stop => match &self.work {
Some(work) => Command::cancel_task(work.id),
None => Command::none(),
},
FileManagerMsg::Refresh => self.refresh(wrap),
FileManagerMsg::Name(value) => {
if let Some(naming) = &mut self.naming {
naming.value = value;
}
Command::none()
}
FileManagerMsg::Submit => self.submit(wrap),
FileManagerMsg::CloseNaming => {
self.naming = None;
Command::none()
}
FileManagerMsg::Done(results) => self.done(results, wrap),
FileManagerMsg::Changed(run, batch) => super::watch::changed(self, run, batch, wrap),
FileManagerMsg::Enter(key) => self.enter(&key, wrap),
FileManagerMsg::Leave => {
if self.shown == ROOT {
return Command::none();
}
let left = self.shown.clone();
let up = parent_key(&left).to_owned();
let command = self.enter(&up, wrap);
self.selected = Some(left);
command
}
FileManagerMsg::Detail(keys) => self.read_details(keys, wrap),
FileManagerMsg::Detailed(read) => {
for (key, details) in read {
self.reading.remove(&key);
self.details.insert(key, details);
}
Command::none()
}
}
}
fn ask_name<Msg: Clone + Send + 'static>(
&mut self,
purpose: NameFor,
folder: String,
value: String,
wrap: &Wrap<Msg>,
) -> Command<Msg> {
let read = folder != ROOT && self.expand(&folder, true);
let command = if read { self.read_folder(&folder, wrap) } else { Command::none() };
self.naming = Some(Naming { purpose, folder, value, tried: false });
command
}
fn submit<Msg: Clone + Send + 'static>(&mut self, wrap: &Wrap<Msg>) -> Command<Msg> {
let Some(naming) = self.naming.as_mut() else { return Command::none() };
naming.tried = true;
if self.naming_problem().is_some() {
return Command::none();
}
let Some(Naming { purpose, folder, value: name, .. }) = self.naming.take() else { return Command::none() };
let confined = self.confined;
match purpose {
NameFor::File => self.run_each(vec![folder], wrap, move |root, folder| {
(child_key(folder, &name), ops::create_file(root, folder, &name, confined))
}),
NameFor::Folder => self.run_each(vec![folder], wrap, move |root, folder| {
(child_key(folder, &name), ops::create_folder(root, folder, &name, confined))
}),
NameFor::Rename(key) if name_of(&key) == name => Command::none(),
NameFor::Rename(key) => self
.run_each(vec![key], wrap, move |root, key| (key.to_owned(), ops::rename(root, key, &name, confined))),
}
}
fn move_all<Msg: Clone + Send + 'static>(
&mut self,
keys: Vec<String>,
into: String,
wrap: &Wrap<Msg>,
) -> Command<Msg> {
if keys.is_empty() {
return Command::none();
}
let confined = self.confined;
self.run_each(keys, wrap, move |root, key| (key.to_owned(), ops::move_into(root, key, &into, confined)))
}
fn copy_all<Msg: Clone + Send + 'static>(
&mut self,
keys: Vec<String>,
into: String,
wrap: &Wrap<Msg>,
) -> Command<Msg> {
if keys.is_empty() || self.work.is_some() {
return Command::none();
}
let (confined, root, count) = (self.confined, self.root.clone(), keys.len());
let label = crate::t!("quvyta.file-manager.copying", n = count);
let finish = Arc::clone(wrap);
let events = Arc::clone(wrap);
let task = Task::new(label, move |cx| {
let mut results = Vec::new();
let step = 1.0 / count as f32;
for (index, key) in keys.iter().enumerate() {
cx.note(name_of(key).to_owned());
let base = index as f32 * step;
let progress = |done: u64, total: u64| {
let share = if total == 0 { 1.0 } else { done as f32 / total as f32 };
cx.progress(base + share * step);
};
let stopped = || cx.is_cancelled();
let watch = ops::Watch { progress: &progress, stopped: &stopped };
results.push((key.clone(), ops::copy_watched(&root, key, &into, confined, &watch)));
if cx.is_cancelled() {
break;
}
}
Ok(finish(FileManagerMsg::Done(results)))
})
.on_event(move |event| events(FileManagerMsg::Work(event)));
self.work = Some(FileWork { id: task.id(), entries: count, done: 0.0, note: String::new() });
Command::task(task)
}
fn took_event<Msg: Clone + Send + 'static>(&mut self, event: TaskEvent, wrap: &Wrap<Msg>) -> Command<Msg> {
let Some(work) = &mut self.work else { return Command::none() };
if event.id() != work.id {
return Command::none();
}
match event {
TaskEvent::Started { .. } => Command::none(),
TaskEvent::Progress { fraction, note, .. } => {
if let Some(fraction) = fraction {
work.done = fraction;
}
if let Some(note) = note {
work.note = note;
}
Command::none()
}
TaskEvent::Finished { outcome, .. } => {
self.work = None;
match outcome {
TaskOutcome::Cancelled => self.refresh(wrap),
TaskOutcome::Done | TaskOutcome::Failed(_) => Command::none(),
}
}
}
}
fn trash_all<Msg: Clone + Send + 'static>(&mut self, keys: Vec<String>, wrap: &Wrap<Msg>) -> Command<Msg> {
let Some(trash) = self.trash.folder() else {
return self.ask_delete_forever(keys, wrap);
};
if keys.is_empty() {
return Command::none();
}
let confined = self.confined;
self.run_each(keys, wrap, move |root, key| (key.to_owned(), ops::to_trash(&trash, root, key, confined)))
}
fn ask_delete<Msg: Clone + Send + 'static>(&self, keys: Vec<String>, wrap: &Wrap<Msg>) -> Command<Msg> {
let folders = keys.iter().any(|key| self.is_folder(key));
let (title, message) = match keys.as_slice() {
[] => return Command::none(),
[key] => {
let name = name_of(key);
let text = if folders {
"quvyta.file-manager.delete-folder-text"
} else {
"quvyta.file-manager.delete-file-text"
};
(crate::t!("quvyta.file-manager.delete-title", name = name), crate::t!(text, name = name))
}
many => {
let mut names = many.iter().take(NAMES_SHOWN).map(|key| name_of(key)).collect::<Vec<_>>().join(", ");
if many.len() > NAMES_SHOWN {
names =
format!("{names} {}", crate::t!("quvyta.file-manager.and-more", n = many.len() - NAMES_SHOWN));
}
let text = if folders {
"quvyta.file-manager.delete-many-folders-text"
} else {
"quvyta.file-manager.delete-many-text"
};
(
crate::t!("quvyta.file-manager.delete-many-title", n = many.len()),
crate::t!(text, names = names.as_str()),
)
}
};
let label = if keys.len() > 1 {
crate::t!("quvyta.file-manager.delete-many", n = keys.len())
} else {
crate::t!("quvyta.file-manager.delete")
};
let confirmed = wrap(FileManagerMsg::DeleteConfirmed(keys));
Command::confirm(Confirm::new(title, confirmed).message(message).confirm_label(label).danger())
}
fn ask_delete_forever<Msg: Clone + Send + 'static>(&self, keys: Vec<String>, wrap: &Wrap<Msg>) -> Command<Msg> {
let (title, message) = match keys.as_slice() {
[] => return Command::none(),
[key] => {
let name = name_of(key);
(
crate::t!("quvyta.file-manager.no-trash-title", name = name),
crate::t!("quvyta.file-manager.no-trash-text", name = name),
)
}
many => (
crate::t!("quvyta.file-manager.no-trash-many-title", n = many.len()),
crate::t!("quvyta.file-manager.no-trash-many-text"),
),
};
let confirmed = wrap(FileManagerMsg::DeleteConfirmed(keys));
let label = crate::t!("quvyta.file-manager.delete-forever");
Command::confirm(Confirm::new(title, confirmed).message(message).confirm_label(label).danger())
}
fn run_each<Msg: Clone + Send + 'static>(
&self,
items: Vec<String>,
wrap: &Wrap<Msg>,
work: impl Fn(&Path, &str) -> (String, Result<FileChange, FileError>) + Send + 'static,
) -> Command<Msg> {
let (root, wrap) = (self.root.clone(), Arc::clone(wrap));
Command::perform(move || wrap(FileManagerMsg::Done(items.iter().map(|item| work(&root, item)).collect())))
}
fn done<Msg: Clone + Send + 'static>(
&mut self,
results: Vec<(String, Result<FileChange, FileError>)>,
wrap: &Wrap<Msg>,
) -> Command<Msg> {
let total = results.len();
let mut touched = Vec::new();
let mut arrived = Vec::new();
let mut refused = Vec::new();
let mut without_trash = Vec::new();
for (key, result) in results {
match result {
Err(FileError::NoTrash) => without_trash.push(key),
Err(error) => refused.push((key, error)),
Ok(FileChange::Copied(key)) => {
touched.push(parent_key(&key).to_owned());
arrived.push(key);
}
Ok(FileChange::Trashed(key)) => {
self.forget(&key);
touched.push(parent_key(&key).to_owned());
}
Ok(FileChange::Created(key)) => {
touched.push(parent_key(&key).to_owned());
arrived.push(key);
}
Ok(FileChange::Moved(from, to)) => {
self.cut.retain(|cut| *cut != from);
if from == to {
continue;
}
self.rekey(&from, &to);
let parent = parent_key(&to).to_owned();
if parent != ROOT {
self.open.insert(parent.clone());
}
touched.extend([parent_key(&from).to_owned(), parent]);
arrived.push(to);
}
Ok(FileChange::Deleted(key)) => {
self.forget(&key);
touched.push(parent_key(&key).to_owned());
}
}
}
if let Some(first) = arrived.first() {
self.selected = Some(first.clone());
self.chosen = arrived;
}
touched.sort();
touched.dedup();
let total = total - without_trash.len();
let asked = self.ask_delete_forever(without_trash, wrap);
Command::batch([refusal(total, &refused), asked, self.reread(touched, wrap)])
}
}
fn refusal<Msg: Clone + Send + 'static>(total: usize, refused: &[(String, FileError)]) -> Command<Msg> {
match refused {
[] => Command::none(),
[(_, error)] if total == 1 => {
Command::toast(Toast::danger(crate::t!("quvyta.file-manager.failed")).body(error.message()))
}
_ => {
let lines: Vec<String> =
refused.iter().map(|(key, error)| format!("{}: {}", name_of(key), error.message())).collect();
let title = crate::t!("quvyta.file-manager.failed-some", failed = refused.len(), total = total);
Command::toast(Toast::danger(title).body(lines.join("\n")))
}
}
}