Skip to content

Commit

Permalink
add ability to create files
Browse files Browse the repository at this point in the history
  • Loading branch information
conaticus committed Jul 2, 2023
1 parent 45a277a commit 8e5f985
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 108 deletions.
59 changes: 59 additions & 0 deletions src-tauri/src/filesystem/explorer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::fs;
use std::fs::read_dir;
use crate::errors::Error;
use crate::filesystem::volume::DirectoryChild;
use crate::filesystem::volume::DirectoryChild::File;

/// Opens a file at the given path. Returns a string if there was an error.
// NOTE(conaticus): I tried handling the errors nicely here but Tauri was mega cringe and wouldn't let me nest results in async functions, so used string error messages instead.
#[tauri::command]
pub async fn open_file(path: String) -> Result<(), Error> {
let output_res = open::commands(path)[0].output();
let output = match output_res {
Ok(output) => output,
Err(err) => {
let err_msg = format!("Failed to get open command output: {}", err);
return Err(Error::Custom(err_msg));
}
};

if output.status.success() {
return Ok(());
}

let err_msg = String::from_utf8(output.stderr).unwrap_or(String::from("Failed to open file and deserialize stderr."));
Err(Error::Custom(err_msg))
}

/// Searches and returns the files in a given directory. This is not recursive.
#[tauri::command]
pub async fn open_directory(path: String) -> Result<Vec<DirectoryChild>, ()> {
let Ok(directory) = read_dir(path) else {
return Ok(Vec::new());
};

Ok(directory
.map(|entry| {
let entry = entry.unwrap();

let file_name = entry.file_name().to_string_lossy().to_string();
let entry_is_file = entry.file_type().unwrap().is_file();
let entry = entry.path().to_string_lossy().to_string();

if entry_is_file {
return DirectoryChild::File(file_name, entry);
}

DirectoryChild::Directory(file_name, entry)
})
.collect())
}

#[tauri::command]
pub async fn create_file(path: String) -> Result<(), Error> {
let res = fs::File::create(path);
match res {
Ok(_) => Ok(()),
Err(err) => Err(Error::Custom(err.to_string())),
}
}
52 changes: 2 additions & 50 deletions src-tauri/src/filesystem/mod.rs
Original file line number Diff line number Diff line change
@@ -1,58 +1,10 @@
pub mod explorer;
pub mod cache;
pub mod volume;

use crate::filesystem::volume::DirectoryChild;
use std::fs::read_dir;
use crate::errors::Error;

pub const DIRECTORY: &str = "directory";
pub const FILE: &str = "file";

pub const fn bytes_to_gb(bytes: u64) -> u16 {
(bytes / (1e+9 as u64)) as u16
}

/// Opens a file at the given path. Returns a string if there was an error.
// NOTE(conaticus): I tried handling the errors nicely here but Tauri was mega cringe and wouldn't let me nest results in async functions, so used string error messages instead.
#[tauri::command]
pub async fn open_file(path: String) -> Result<(), Error> {
let output_res = open::commands(path)[0].output();
let output = match output_res {
Ok(output) => output,
Err(err) => {
let err_msg = format!("Failed to get open command output: {}", err);
return Err(Error::Custom(err_msg));
}
};

if output.status.success() {
return Ok(());
}

let err_msg = String::from_utf8(output.stderr).unwrap_or(String::from("Failed to open file and deserialize stderr."));
Err(Error::Custom(err_msg))
}

/// Searches and returns the files in a given directory. This is not recursive.
#[tauri::command]
pub async fn open_directory(path: String) -> Result<Vec<DirectoryChild>, ()> {
let Ok(directory) = read_dir(path) else {
return Ok(Vec::new());
};

Ok(directory
.map(|entry| {
let entry = entry.unwrap();

let file_name = entry.file_name().to_string_lossy().to_string();
let entry_is_file = entry.file_type().unwrap().is_file();
let entry = entry.path().to_string_lossy().to_string();

if entry_is_file {
return DirectoryChild::File(file_name, entry);
}

DirectoryChild::Directory(file_name, entry)
})
.collect())
}
}
5 changes: 3 additions & 2 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod filesystem;
mod search;
mod errors;

use filesystem::{open_directory, open_file};
use filesystem::explorer::{open_file, open_directory, create_file};
use filesystem::volume::get_volumes;
use search::search_directory;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -36,7 +36,8 @@ async fn main() {
get_volumes,
open_directory,
search_directory,
open_file
open_file,
create_file
])
.manage(Arc::new(Mutex::new(AppState::default())))
.run(tauri::generate_context!())
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"withGlobalTauri": false
},
"package": {
"productName": "file-explorer",
"productName": "File Explorer",
"version": "0.0.0"
},
"tauri": {
Expand Down
35 changes: 20 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import {useEffect, useState} from "react";
import {invoke} from "@tauri-apps/api/tauri";
import {DirectoryContent, Volume} from "./types";
import {openDirectory} from "./ipc/fileExplorer";
import {openDirectory} from "./ipc";
import VolumeList from "./components/MainBody/Volumes/VolumeList";
import FolderNavigation from "./components/TopBar/FolderNavigation";
import {DirectoryContents} from "./components/MainBody/DirectoryContents";
import useNavigation from "./hooks/useNavigation";
import SearchBar from "./components/TopBar/SearchBar";
import {useAppDispatch} from "./state/hooks";
import {useAppDispatch, useAppSelector} from "./state/hooks";
import useContextMenu from "./hooks/useContextMenu";
import ContextMenus from "./components/ContextMenus/ContextMenus";
import {
selectDirectoryContents,
unselectDirectoryContents,
updateDirectoryContents
} from "./state/slices/currentDirectorySlice";

function App() {
const [volumes, setVolumes] = useState<Volume[]>([]);
const [directoryContents, setDirectoryContents] = useState<
DirectoryContent[]
>([]);
const directoryContents = useAppSelector(selectDirectoryContents);
const dispatch = useAppDispatch();

const [searchResults, setSearchResults] = useState<DirectoryContent[]>([]);

Expand All @@ -31,9 +35,9 @@ function App() {
setCurrentVolume,
} = useNavigation(searchResults, setSearchResults);

async function updateDirectoryContents() {
async function getNewDirectoryContents() {
const contents = await openDirectory(pathHistory[historyPlace]);
setDirectoryContents(contents);
dispatch(updateDirectoryContents(contents));
}

async function onVolumeClick(mountpoint: string) {
Expand All @@ -43,8 +47,7 @@ function App() {
setHistoryPlace(pathHistory.length - 1);
setCurrentVolume(mountpoint);

const directoryContents = await openDirectory(pathHistory[historyPlace]);
setDirectoryContents(directoryContents);
await getNewDirectoryContents();
}

async function onDirectoryClick(filePath: string) {
Expand All @@ -55,7 +58,7 @@ function App() {
pathHistory.push(filePath);
setHistoryPlace(pathHistory.length - 1);

await updateDirectoryContents();
await getNewDirectoryContents();
}

async function getVolumes() {
Expand Down Expand Up @@ -83,14 +86,16 @@ function App() {
return;
}

updateDirectoryContents().catch(console.error);
getNewDirectoryContents().catch(console.error);
}, [historyPlace]);

const dispatch = useAppDispatch();
const [handleMainContextMenu, handleCloseContextMenu] = useContextMenu(dispatch);
const [handleMainContextMenu, handleCloseContextMenu] = useContextMenu(dispatch, pathHistory[historyPlace]);

return (
<div className="h-full" onClick={handleCloseContextMenu} onContextMenu={handleMainContextMenu}>
<div className="h-full" onClick={(e) => {
dispatch(unselectDirectoryContents());
handleCloseContextMenu(e);
}} onContextMenu={handleMainContextMenu}>
<ContextMenus />

<div className="p-4">
Expand All @@ -108,7 +113,7 @@ function App() {
setSearchResults={setSearchResults}
/>

<div className="w-full">
<div className="w-7/12">
{pathHistory[historyPlace] === "" && searchResults.length === 0 ? (
<VolumeList volumes={volumes} onClick={onVolumeClick} />
) : (
Expand Down
6 changes: 3 additions & 3 deletions src/components/ContextMenus/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ export default function ContextMenu({ options }: Props) {
const { mouseX, mouseY } = useAppSelector(state => state.contextMenu);

return (
<div id="context-menu" className="bg-darker w-56" style={{
<div id="context-menu" className="bg-darker w-48" style={{
position: "absolute",
left: mouseX,
top: mouseY,
}}>
{options.map((option, idx) => (
<div key={idx}>
<div key={idx} className="">
<button onClick={() => {
option.onClick();
dispatch(updateContextMenu(NO_CONTEXT_MENU));
}} className="bg-darker hover:bg-gray-600 w-full">{option.name}</button>
}} className="bg-darker hover:bg-bright w-full">{option.name}</button>
<br />
</div>
))}
Expand Down
86 changes: 67 additions & 19 deletions src/components/ContextMenus/ContextMenus.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,72 @@
import {ContextMenuType} from "../../types";
import {ContextMenuType, DirectoryContent} from "../../types";
import ContextMenu from "./ContextMenu";
import {useAppSelector} from "../../state/hooks";
import {selectCurrentContextMenu} from "../../state/slices/contextMenuSlice";
import {useAppDispatch, useAppSelector} from "../../state/hooks";
import InputModal from "../InputModal";
import {useState} from "react";
import {confirm} from "@tauri-apps/api/dialog";
import {
ContextMenuState,
DirectoryEntityContextPayload,
GeneralContextPayload
} from "../../state/slices/contextMenuSlice";
import {createFile} from "../../ipc";
import {addContent, selectContentIdx} from "../../state/slices/currentDirectorySlice";

export default function ContextMenus() {
const currentContextMenu = useAppSelector(selectCurrentContextMenu);

switch (currentContextMenu) {
case ContextMenuType.General: return (
<ContextMenu options={[
{ name: "General Opt 1", onClick: () => {} },
{ name: "General Opt 2", onClick: () => {} }
]} />
)
case ContextMenuType.DirectoryEntity: return (
<ContextMenu options={[
{ name: "Entity Opt 1", onClick: () => {} },
{ name: "Entity Opt 2", onClick: () => {} }
]} />
)
default: return <></>;
const { currentContextMenu, contextMenuPayload } = useAppSelector(state => state.contextMenu);
const [newFileShown, setNewFileShown] = useState(false);
const [newDirectoryShown, setNewDirectoryShown] = useState(false);
const [renameFileShown, setRenameFileShown] = useState(false);

// Typescript pain
const directoryEntityPayload = contextMenuPayload as DirectoryEntityContextPayload;
const generalPayload = contextMenuPayload as GeneralContextPayload;

const dispatch = useAppDispatch();

async function onNewFile(name: string) {
try {
const path = generalPayload.currentPath + name;
await createFile(path);

const newDirectoryContent = {"File": [name, path]} as DirectoryContent;
dispatch(addContent(newDirectoryContent));
dispatch(selectContentIdx(0)) // Select top item as content is added to the top.
} catch (e) {
alert(e);
}
}

function onNewFolder(name: string) {

}

function onRename(name: string) {

}

return (
<>
{currentContextMenu == ContextMenuType.General ? (
<ContextMenu options={[
{ name: "New File", onClick: () => setNewFileShown(true) },
{ name: "New Folder", onClick: () => setNewDirectoryShown(true)}
]} />
) : currentContextMenu == ContextMenuType.DirectoryEntity ? (
<ContextMenu options={[
{ name: "Rename", onClick: () => setRenameFileShown(true) },
{ name: "Delete", onClick: async () => {
const result = await confirm("Are you sure you want to delete this file?");
if (result) {
// Delete file
}
}}
]} />
) : ""}

<InputModal shown={newFileShown} setShown={setNewFileShown} title="New File" onSubmit={onNewFile} submitName="Create" />
<InputModal shown={newDirectoryShown} setShown={setNewDirectoryShown} title="New Folder" onSubmit={onNewFolder} submitName="Create" />
<InputModal shown={renameFileShown} setShown={setRenameFileShown} title="Rename File" onSubmit={onRename} submitName="Rename" />
</>
)
}
4 changes: 3 additions & 1 deletion src/components/MainBody/DirectoryContents.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import DirectoryEntity from "./DirectoryEntity";
import {DirectoryContent} from "../../types";
import {openFile} from "../../ipc/fileExplorer";
import {openFile} from "../../ipc";

interface Props {
content: DirectoryContent[];
Expand All @@ -27,7 +27,9 @@ export function DirectoryContents({content, onDirectoryClick}: Props) {
: onFileClick(filePath)
}
key={idx}
idx={idx}
name={fileName}
path={filePath}
/>
);
})}
Expand Down
Loading

0 comments on commit 8e5f985

Please sign in to comment.