After a fun terminal app, let's build a file manager with Electron and Svelte.
We'll start small, by just displaying list of files in the current directory, and letting user navigate to other directories by clicking.
We'll start with Svelte setup from previous episodes. I think I'll have a whole episode about various boilerplate generators, for now you can just copy the files over from episode 13 and clean up anything speific to that one.
preload.js
We need to export two function. First, we need to let the app know where we started, and fortunately that's very easy:
let currentDirectory = () => {
return process.cwd()
}
Second we need to return list of files in directory app wants to show. Node has multiple awkward APIs for that, and the least awkward one is one from fs/promises
.
Unfortunately Dirent
objects it returns do not survive travel from preload code to the frontend, and we need to turn what it returns into a plain Javascript object. I'm not completely sure why it doesn't work, I'm guessing something to do with how context isolation works.
let { readdir } = require("fs/promises")
let directoryContents = async (path) => {
let results = await readdir(path, {withFileTypes: true})
return results.map(entry => ({
name: entry.name,
type: entry.isDirectory() ? "directory" : "file",
}))
}
Now we just need to tell preload.js
to expose both:
let { contextBridge } = require("electron")
contextBridge.exposeInMainWorld(
"api", { directoryContents, currentDirectory }
)
App.js
And here's the app itself. We're using Svelte's features aggressively here.
<script>
let directory = window.api.currentDirectory()
$: filesPromise = window.api.directoryContents(directory)
$: isRoot = (directory === "/")
function navigate(path) {
if (directory === "/") {
directory = "/" + path
} else {
directory += "/" + path
}
}
function navigateUp() {
directory = directory.split("/").slice(0, -1).join("/") || "/"
}
</script>
<h1>{directory}</h1>
{#await filesPromise}
{:then files}
{#if !isRoot}
<div><button on:click={() => navigateUp()}>..</button></div>
{/if}
{#each files as entry}
{#if entry.type === "directory"}
<div>
<button on:click={() => navigate(entry.name)}>{entry.name}</button>
</div>
{:else}
<div>{entry.name}</div>
{/if}
{/each}
{/await}
<style>
:global(body) {
background-color: #444;
color: #ccc;
}
</style>
Let's explain some things. There's a bit of path manipulation - there are libraries for it, but I didn't want to do anything complicated, so I'm just assuming we're on a Mac, or a Linux, or such system with /
separators.
Simply adding /path
or removing last /path
changes directory up or down - except we need to treat root directory specially as it's /
not an empty string.
After that everything else like creating new promise filesPromise
, resolving it to files
, and setting isRoot
is handled by Svelte's reactivity.
If you're coming from React background, it would take a few useEffect
and usePromise
calls that we don't need to do, as Svelte figures it all out. Or we could move a lot of that logic to a store or custom hook, or such, but Svelte is expressive enough that a regular component will do just fine, at least for now.
Results
Here's the result:
I plan to add a lot of feature to this app, but just for fun, for the next episode let's do the same thing in React and see how it compares.
As usual, all the code for the episode is here.
Top comments (0)