mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-18 11:59:11 +00:00
add frontend ground work for cut/copy actions
This commit is contained in:
parent
b9f4d59bf4
commit
6b5af02b87
|
@ -2,6 +2,7 @@ use crate::{
|
||||||
invalidate_query,
|
invalidate_query,
|
||||||
job::Job,
|
job::Job,
|
||||||
object::fs::{
|
object::fs::{
|
||||||
|
copy::{FileCopierJob, FileCopierJobInit},
|
||||||
decrypt::{FileDecryptorJob, FileDecryptorJobInit},
|
decrypt::{FileDecryptorJob, FileDecryptorJobInit},
|
||||||
delete::{FileDeleterJob, FileDeleterJobInit},
|
delete::{FileDeleterJob, FileDeleterJobInit},
|
||||||
duplicate::{FileDuplicatorJob, FileDuplicatorJobInit},
|
duplicate::{FileDuplicatorJob, FileDuplicatorJobInit},
|
||||||
|
@ -133,6 +134,14 @@ pub(crate) fn mount() -> RouterBuilder {
|
||||||
.await;
|
.await;
|
||||||
invalidate_query!(library, "locations.getExplorerData");
|
invalidate_query!(library, "locations.getExplorerData");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.library_mutation("copyFiles", |t| {
|
||||||
|
t(|_, args: FileCopierJobInit, library| async move {
|
||||||
|
library.spawn_job(Job::new(args, FileCopierJob {})).await;
|
||||||
|
invalidate_query!(library, "locations.getExplorerData");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
use super::{context_menu_fs_info, FsInfo, ObjectType};
|
||||||
|
use crate::job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
use std::{collections::VecDeque, hash::Hash, path::PathBuf};
|
||||||
|
|
||||||
|
pub struct FileCopierJob {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct FileCopierJobState {
|
||||||
|
pub root_path: PathBuf,
|
||||||
|
pub root_prefix: PathBuf,
|
||||||
|
pub root_type: ObjectType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Hash, Type)]
|
||||||
|
pub struct FileCopierJobInit {
|
||||||
|
pub source_location_id: i32,
|
||||||
|
pub source_path_id: i32,
|
||||||
|
pub target_location_id: i32,
|
||||||
|
pub target_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct FileCopierJobStep {
|
||||||
|
pub fs_info: FsInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
const JOB_NAME: &str = "file_copier";
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl StatefulJob for FileCopierJob {
|
||||||
|
type Data = FileCopierJobState;
|
||||||
|
type Init = FileCopierJobInit;
|
||||||
|
type Step = FileCopierJobStep;
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
JOB_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(&self, ctx: WorkerContext, state: &mut JobState<Self>) -> Result<(), JobError> {
|
||||||
|
// let fs_info = context_menu_fs_info(
|
||||||
|
// &ctx.library_ctx.db,
|
||||||
|
// state.init.location_id,
|
||||||
|
// state.init.path_id,
|
||||||
|
// )
|
||||||
|
// .await?;
|
||||||
|
|
||||||
|
// let root_prefix = if fs_info.obj_type == ObjectType::File {
|
||||||
|
// let mut output_path = fs_info.obj_path.clone();
|
||||||
|
// output_path.set_file_name(
|
||||||
|
// fs_info
|
||||||
|
// .obj_path
|
||||||
|
// .file_stem()
|
||||||
|
// .unwrap()
|
||||||
|
// .to_str()
|
||||||
|
// .unwrap()
|
||||||
|
// .to_string() + "-Copy"
|
||||||
|
// + &fs_info.obj_path.extension().map_or_else(
|
||||||
|
// || String::from(""),
|
||||||
|
// |x| String::from(".") + x.to_str().unwrap(),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// output_path
|
||||||
|
// } else {
|
||||||
|
// let mut output_path = fs_info.obj_path.clone();
|
||||||
|
// output_path.set_file_name(
|
||||||
|
// output_path
|
||||||
|
// .file_stem()
|
||||||
|
// .unwrap()
|
||||||
|
// .to_str()
|
||||||
|
// .unwrap()
|
||||||
|
// .to_string() + "-Copy",
|
||||||
|
// );
|
||||||
|
// output_path
|
||||||
|
// };
|
||||||
|
|
||||||
|
// state.data = Some(FileCopierJobState {
|
||||||
|
// root_path: fs_info.obj_path.clone(),
|
||||||
|
// root_prefix,
|
||||||
|
// root_type: fs_info.obj_type.clone(),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// state.steps = VecDeque::new();
|
||||||
|
// state.steps.push_back(FileCopierJobStep { fs_info });
|
||||||
|
|
||||||
|
// ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_step(
|
||||||
|
&self,
|
||||||
|
ctx: WorkerContext,
|
||||||
|
state: &mut JobState<Self>,
|
||||||
|
) -> Result<(), JobError> {
|
||||||
|
// let step = state.steps[0].clone();
|
||||||
|
// let info = &step.fs_info;
|
||||||
|
|
||||||
|
// let job_state = state.data.clone().ok_or(JobError::MissingData {
|
||||||
|
// value: String::from("job state"),
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// match info.obj_type {
|
||||||
|
// ObjectType::File => {
|
||||||
|
// let mut path = job_state.root_prefix.clone();
|
||||||
|
|
||||||
|
// if job_state.root_type == ObjectType::Directory {
|
||||||
|
// path.push(
|
||||||
|
// info.obj_path
|
||||||
|
// .strip_prefix(job_state.root_path.clone())
|
||||||
|
// .unwrap(),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// std::fs::copy(info.obj_path.clone(), path.clone())?;
|
||||||
|
// }
|
||||||
|
// ObjectType::Directory => {
|
||||||
|
// for entry in std::fs::read_dir(info.obj_path.clone())? {
|
||||||
|
// let entry = entry?;
|
||||||
|
// if entry.metadata()?.is_dir() {
|
||||||
|
// let obj_type = ObjectType::Directory;
|
||||||
|
// state.steps.push_back(FileCopierJobStep {
|
||||||
|
// fs_info: FsInfo {
|
||||||
|
// obj_id: None,
|
||||||
|
// obj_name: String::new(),
|
||||||
|
// obj_path: entry.path(),
|
||||||
|
// obj_type,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// let path_suffix = entry
|
||||||
|
// .path()
|
||||||
|
// .strip_prefix(job_state.root_path.clone())
|
||||||
|
// .unwrap()
|
||||||
|
// .to_path_buf();
|
||||||
|
|
||||||
|
// let mut path = job_state.root_prefix.clone();
|
||||||
|
// path.push(path_suffix);
|
||||||
|
// std::fs::create_dir_all(path)?;
|
||||||
|
// } else {
|
||||||
|
// let obj_type = ObjectType::File;
|
||||||
|
// state.steps.push_back(FileCopierJobStep {
|
||||||
|
// fs_info: FsInfo {
|
||||||
|
// obj_id: None,
|
||||||
|
// obj_name: entry.file_name().to_str().unwrap().to_string(),
|
||||||
|
// obj_path: entry.path(),
|
||||||
|
// obj_type,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// ctx.progress(vec![JobReportUpdate::TaskCount(state.steps.len())]);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// ctx.progress(vec![JobReportUpdate::CompletedTaskCount(
|
||||||
|
// state.step_number + 1,
|
||||||
|
// )]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finalize(&self, _ctx: WorkerContext, state: &mut JobState<Self>) -> JobResult {
|
||||||
|
Ok(Some(serde_json::to_value(&state.init)?))
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ use crate::{
|
||||||
prisma::{file_path, location, PrismaClient},
|
prisma::{file_path, location, PrismaClient},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod copy;
|
||||||
pub mod decrypt;
|
pub mod decrypt;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
pub mod duplicate;
|
pub mod duplicate;
|
||||||
|
|
|
@ -82,6 +82,7 @@ export type Procedures = {
|
||||||
| { key: 'tags.list'; input: LibraryArgs<null>; result: Array<Tag> }
|
| { key: 'tags.list'; input: LibraryArgs<null>; result: Array<Tag> }
|
||||||
| { key: 'volumes.list'; input: never; result: Array<Volume> };
|
| { key: 'volumes.list'; input: never; result: Array<Volume> };
|
||||||
mutations:
|
mutations:
|
||||||
|
| { key: 'files.copyFiles'; input: LibraryArgs<FileCopierJobInit>; result: null }
|
||||||
| { key: 'files.decryptFiles'; input: LibraryArgs<FileDecryptorJobInit>; result: null }
|
| { key: 'files.decryptFiles'; input: LibraryArgs<FileDecryptorJobInit>; result: null }
|
||||||
| { key: 'files.delete'; input: LibraryArgs<number>; result: null }
|
| { key: 'files.delete'; input: LibraryArgs<number>; result: null }
|
||||||
| { key: 'files.deleteFiles'; input: LibraryArgs<FileDeleterJobInit>; result: null }
|
| { key: 'files.deleteFiles'; input: LibraryArgs<FileDeleterJobInit>; result: null }
|
||||||
|
@ -181,6 +182,13 @@ export type ExplorerItem =
|
||||||
| ({ type: 'Path' } & FilePathWithObject)
|
| ({ type: 'Path' } & FilePathWithObject)
|
||||||
| ({ type: 'Object' } & ObjectWithFilePaths);
|
| ({ type: 'Object' } & ObjectWithFilePaths);
|
||||||
|
|
||||||
|
export interface FileCopierJobInit {
|
||||||
|
source_location_id: number;
|
||||||
|
source_path_id: number;
|
||||||
|
target_location_id: number;
|
||||||
|
target_path: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FileDecryptorJobInit {
|
export interface FileDecryptorJobInit {
|
||||||
location_id: number;
|
location_id: number;
|
||||||
path_id: number;
|
path_id: number;
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import {
|
import {
|
||||||
ArrowBendUpRight,
|
ArrowBendUpRight,
|
||||||
|
Clipboard,
|
||||||
|
Copy,
|
||||||
|
FileX,
|
||||||
Image,
|
Image,
|
||||||
LockSimple,
|
LockSimple,
|
||||||
LockSimpleOpen,
|
LockSimpleOpen,
|
||||||
Package,
|
Package,
|
||||||
Plus,
|
Plus,
|
||||||
Repeat,
|
Repeat,
|
||||||
|
Scissors,
|
||||||
Share,
|
Share,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
TagSimple,
|
TagSimple,
|
||||||
|
@ -101,6 +105,7 @@ export function ExplorerContextMenu(props: PropsWithChildren) {
|
||||||
const generateThumbsForLocation = useLibraryMutation('jobs.generateThumbsForLocation');
|
const generateThumbsForLocation = useLibraryMutation('jobs.generateThumbsForLocation');
|
||||||
const objectValidator = useLibraryMutation('jobs.objectValidator');
|
const objectValidator = useLibraryMutation('jobs.objectValidator');
|
||||||
const rescanLocation = useLibraryMutation('locations.fullRescan');
|
const rescanLocation = useLibraryMutation('locations.fullRescan');
|
||||||
|
const copyFiles = useLibraryMutation('files.copyFiles');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
@ -131,6 +136,38 @@ export function ExplorerContextMenu(props: PropsWithChildren) {
|
||||||
icon={Repeat}
|
icon={Repeat}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CM.Item
|
||||||
|
label="Paste"
|
||||||
|
keybind="⌘V"
|
||||||
|
hidden={!store.cutCopyState.active}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (store.cutCopyState.actionType == CutCopyType.Copy) {
|
||||||
|
store.locationId &&
|
||||||
|
copyFiles.mutate({
|
||||||
|
source_location_id: store.cutCopyState.sourceLocationId,
|
||||||
|
source_path_id: store.cutCopyState.sourcePathId,
|
||||||
|
target_location_id: store.locationId,
|
||||||
|
target_path: params.path
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// cut here
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
icon={Clipboard}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CM.Item
|
||||||
|
label="Deselect"
|
||||||
|
hidden={!store.cutCopyState.active}
|
||||||
|
onClick={(e) => {
|
||||||
|
getExplorerStore().cutCopyState = {
|
||||||
|
...store.cutCopyState,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
icon={FileX}
|
||||||
|
/>
|
||||||
|
|
||||||
<CM.SubMenu label="More actions..." icon={Plus}>
|
<CM.SubMenu label="More actions..." icon={Plus}>
|
||||||
<CM.Item
|
<CM.Item
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
@ -198,6 +235,46 @@ export function FileItemContextMenu({ ...props }: FileItemContextMenuProps) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CM.Item
|
||||||
|
label="Cut"
|
||||||
|
keybind="⌘X"
|
||||||
|
onClick={(e) => {
|
||||||
|
getExplorerStore().cutCopyState = {
|
||||||
|
sourceLocationId: expStore.locationId!,
|
||||||
|
sourcePathId: props.item.id,
|
||||||
|
actionType: CutCopyType.Cut,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
icon={Scissors}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CM.Item
|
||||||
|
label="Copy"
|
||||||
|
keybind="⌘C"
|
||||||
|
onClick={(e) => {
|
||||||
|
getExplorerStore().cutCopyState = {
|
||||||
|
sourceLocationId: expStore.locationId!,
|
||||||
|
sourcePathId: props.item.id,
|
||||||
|
actionType: CutCopyType.Copy,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
icon={Copy}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CM.Item
|
||||||
|
label="Deselect"
|
||||||
|
hidden={!expStore.cutCopyState.active}
|
||||||
|
onClick={(e) => {
|
||||||
|
getExplorerStore().cutCopyState = {
|
||||||
|
...expStore.cutCopyState,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
icon={FileX}
|
||||||
|
/>
|
||||||
|
|
||||||
<CM.Separator />
|
<CM.Separator />
|
||||||
|
|
||||||
<CM.Item
|
<CM.Item
|
||||||
|
|
|
@ -10,6 +10,11 @@ export enum ExplorerKind {
|
||||||
Space
|
Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CutCopyType {
|
||||||
|
Cut,
|
||||||
|
Copy
|
||||||
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
locationId: null as number | null,
|
locationId: null as number | null,
|
||||||
layoutMode: 'grid' as ExplorerLayoutMode,
|
layoutMode: 'grid' as ExplorerLayoutMode,
|
||||||
|
@ -21,7 +26,13 @@ const state = {
|
||||||
multiSelectIndexes: [] as number[],
|
multiSelectIndexes: [] as number[],
|
||||||
contextMenuObjectId: null as number | null,
|
contextMenuObjectId: null as number | null,
|
||||||
contextMenuActiveObject: null as object | null,
|
contextMenuActiveObject: null as object | null,
|
||||||
newThumbnails: {} as Record<string, boolean>
|
newThumbnails: {} as Record<string, boolean>,
|
||||||
|
cutCopyState: {
|
||||||
|
sourceLocationId: 0,
|
||||||
|
sourcePathId: 0,
|
||||||
|
actionType: CutCopyType.Cut,
|
||||||
|
active: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onLibraryChange(() => getExplorerStore().reset());
|
onLibraryChange(() => getExplorerStore().reset());
|
||||||
|
|
Loading…
Reference in a new issue