DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Electron Adventures: Episode 51: Deleting Files

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"],
    },
Enter fullscreen mode Exit fullscreen mode

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])
    }
  }
Enter fullscreen mode Exit fullscreen mode

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}
  }
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

Let's simplify this to:

{#if dialog}
  <Dialog {...dialog} />
{/if}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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:

Episode 51 Screenshot

In the next episode, we'll add support for some error messages.

As usual, all the code for the episode is here.

Discussion (0)