The next operation we need to implement is deleting files - or more accurately moving files to trash, as no file manager in 2021 should actually hard delete files.
src/commands.js
As usual we start by adding a new command to the commands
list:
{
name: "Delete Files",
shortcuts: [{key: "F8"}],
action: ["activePanel", "deleteFiles"],
},
src/Panel.svelte
We need two things from the active Panel
- its currently active directory, and which files we should be deleting.
There are three possibilities. We'll have very similar logic for copying files, moving files, and a lot of other operations, so we should refactor this at some point:
- is any files are selected, operate on those
- if no files are selected, operate on currently focused file
- unless the currently focused one is
..
, then do nothing
function deleteFiles() {
if (selected.length) {
app.openDeleteDialog(directory, selected.map(idx => files[idx].name))
} else if (focused && focused.name !== "..") {
app.openDeleteDialog(directory, [focused.name])
}
}
src/App.svelte
This is our third dialog, and App
has far too many responsibilities to also bother with rendering every possible dialog. For now let's refactor dialog opening code to just this:
function openPalette() {
dialog = {type: "CommandPalette"}
}
function openMkdirDialog(base) {
dialog = {type: "MkdirDialog", base}
}
function openDeleteDialog(base, files) {
dialog = {type: "DeleteDialog", base, files}
}
But maybe we should just have one openDialog
function, and pass that hash there directly? It's something to consider.
If we continued the template how we had it before it would be:
{#if dialog}
{#if dialog.type === "CommandPalette"}
<CommandPalette />
{:else if dialog.type === "MkdirDialog"}
<MkdirDialog base={dialog.base} />
{:else if dialog.type === "DeleteDialog"}
<DeleteDialog base={dialog.base} files={dialog.files} />
{/if}
{/if}
Let's simplify this to:
{#if dialog}
<Dialog {...dialog} />
{/if}
src/Dialog.svelte
But we don't want to just move that ever growing if/else chain into another file. Let's use some metaprogramming to simplify this.
<script>
import CommandPalette from "./CommandPalette.svelte"
import DeleteDialog from "./DeleteDialog.svelte"
import MkdirDialog from "./MkdirDialog.svelte"
let {type, ...otherProps} = $$props
let component = {CommandPalette, MkdirDialog, DeleteDialog}
</script>
<div>
<svelte:component this={component[type]} {...otherProps}/>
</div>
<style>
div {
position: fixed;
left: 0;
top: 0;
right: 0;
margin: auto;
padding: 8px;
max-width: 50vw;
background: #338;
box-shadow: 0px 0px 24px #004;
}
</style>
Svelte normally passes props into individual variables, but you can also access the whole set with $$props
. We do some destructuring to extract type
and get the rest of the props into otherProps
.
Then with <svelte:component this={component[type]} {...otherProps}/>
we tell Svelte to pick the right component, and pass whatever the rest of the props are.
If you somehow mess up the prop list, you'll get a console warning in development mode, but this is the power of dynamic typing. It Just Works, without pages of mindless boilerplate.
Since code for placing the dialog in the right place is already in Dialog
, we can remove it from CommandPalette
, and MkdirDialog
.
Moving files to trash
Moving files to trash is something pretty much every operating system made in the last half century supported (even the ancient MS DOS had rudimentary functionality of this kind), but bafflingly most programming languages including node have no support for it at all!
We'll be using trash
package to do this.
So we need to install it with npm i trash
.
src/DeleteDialog.svelte
The dialog is very similar to the MkdirDialog
dialogs.
The main difference is that now the submit action is async, and quite slow as it needs to launch an external program to actually move the files to the trash, so it's quite slow. It really asks for some kind of feedback that deletion is in progress, and of course error handling. We'll get there of course.
It also feels like we should probably move that button bar to another component, as it's nearly exact copypasta of the ones in MkdirDialog
.
The dialog is a huge improvement over most file managers in that it tells you excatly what it will be deleting. The absolute worst dialogs are: "Are you sure? OK / Cancel". Dialogs "Are you sure you want to delete files? Delete / Cancel" are a bit better. But really we should be very exact, especially with such potentially dangerous actions. Unfortunately what it doesn't handle quite as well is situations where list of files would be too long. We'll get there as well.
<script>
export let base
export let files
import path from "path-browserify"
import { getContext } from "svelte"
let { eventBus } = getContext("app")
let app = eventBus.target("app")
let bothPanels = eventBus.target("bothPanels")
async function submit() {
for (let file of files) {
let fullPath = path.join(base, file)
await window.api.moveFileToTrash(fullPath)
}
app.closeDialog()
bothPanels.refresh()
}
function focus(el) {
el.focus()
}
</script>
<form on:submit|preventDefault={submit}>
<div>Do you want to delete the following files in {base}:</div>
<ul>
{#each files as file}
<li>{file}</li>
{/each}
</ul>
<div class="buttons">
<button type="submit" use:focus>Delete</button>
<button on:click={app.closeDialog}>Cancel</button>
</div>
</form>
<style>
.buttons {
display: flex;
flex-direction: row-reverse;
margin-top: 8px;
gap: 8px;
}
button {
font-family: inherit;
font-size: inherit;
background-color: #66b;
color: inherit;
}
</style>
preload.js
And finally we need to expose the relevant method in the preload:
let trash = require("trash")
let moveFileToTrash = async (file) => {
await trash(file)
}
It's an interesting question if backend or frontend should be doing the looping. In this case backend looping would have a lot better performance, but it would be significantly more difficult to accurately report errors.
Result
Here's the results:
In the next episode, we'll add support for some error messages.
As usual, all the code for the episode is here.
Top comments (0)