add frontend ground work for cut/copy actions

This commit is contained in:
brxken128 2023-01-20 13:18:11 +00:00
parent b9f4d59bf4
commit 6b5af02b87
6 changed files with 274 additions and 1 deletions

View file

@ -2,6 +2,7 @@ use crate::{
invalidate_query,
job::Job,
object::fs::{
copy::{FileCopierJob, FileCopierJobInit},
decrypt::{FileDecryptorJob, FileDecryptorJobInit},
delete::{FileDeleterJob, FileDeleterJobInit},
duplicate::{FileDuplicatorJob, FileDuplicatorJobInit},
@ -133,6 +134,14 @@ pub(crate) fn mount() -> RouterBuilder {
.await;
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(())
})
})

View file

@ -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)?))
}
}

View file

@ -7,6 +7,7 @@ use crate::{
prisma::{file_path, location, PrismaClient},
};
pub mod copy;
pub mod decrypt;
pub mod delete;
pub mod duplicate;

View file

@ -82,6 +82,7 @@ export type Procedures = {
| { key: 'tags.list'; input: LibraryArgs<null>; result: Array<Tag> }
| { key: 'volumes.list'; input: never; result: Array<Volume> };
mutations:
| { key: 'files.copyFiles'; input: LibraryArgs<FileCopierJobInit>; result: null }
| { key: 'files.decryptFiles'; input: LibraryArgs<FileDecryptorJobInit>; result: null }
| { key: 'files.delete'; input: LibraryArgs<number>; result: null }
| { key: 'files.deleteFiles'; input: LibraryArgs<FileDeleterJobInit>; result: null }
@ -181,6 +182,13 @@ export type ExplorerItem =
| ({ type: 'Path' } & FilePathWithObject)
| ({ type: 'Object' } & ObjectWithFilePaths);
export interface FileCopierJobInit {
source_location_id: number;
source_path_id: number;
target_location_id: number;
target_path: string;
}
export interface FileDecryptorJobInit {
location_id: number;
path_id: number;

View file

@ -1,11 +1,15 @@
import {
ArrowBendUpRight,
Clipboard,
Copy,
FileX,
Image,
LockSimple,
LockSimpleOpen,
Package,
Plus,
Repeat,
Scissors,
Share,
ShieldCheck,
TagSimple,
@ -101,6 +105,7 @@ export function ExplorerContextMenu(props: PropsWithChildren) {
const generateThumbsForLocation = useLibraryMutation('jobs.generateThumbsForLocation');
const objectValidator = useLibraryMutation('jobs.objectValidator');
const rescanLocation = useLibraryMutation('locations.fullRescan');
const copyFiles = useLibraryMutation('files.copyFiles');
return (
<div className="relative">
@ -131,6 +136,38 @@ export function ExplorerContextMenu(props: PropsWithChildren) {
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.Item
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.Item

View file

@ -10,6 +10,11 @@ export enum ExplorerKind {
Space
}
export enum CutCopyType {
Cut,
Copy
}
const state = {
locationId: null as number | null,
layoutMode: 'grid' as ExplorerLayoutMode,
@ -21,7 +26,13 @@ const state = {
multiSelectIndexes: [] as number[],
contextMenuObjectId: null as number | 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());