Skip to content

Commit 82e1b10

Browse files
committed
opfs explorer
1 parent e319e52 commit 82e1b10

File tree

2 files changed

+267
-1
lines changed

2 files changed

+267
-1
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
<script lang="ts">
2+
// src from: https://github.com/tomayac/opfs-explorer/blob/main/contentscript.js
3+
4+
type fileType = {
5+
name: string;
6+
kind: string;
7+
size: number;
8+
type: string;
9+
lastModified: number;
10+
relativePath: string;
11+
};
12+
13+
let fileHandles: any[] = [];
14+
let directoryHandles: any[] = [];
15+
16+
let fileList: fileType[] = [];
17+
18+
async function getDirectoryEntriesRecursive(directoryHandle: any, relativePath = '.') {
19+
const entries: any = {};
20+
// Get an iterator of the files and folders in the directory.
21+
const directoryIterator = directoryHandle.values();
22+
const directoryEntryPromises = [];
23+
for await (const handle of directoryIterator) {
24+
const nestedPath = `${relativePath}/${handle.name}`;
25+
if (handle.kind === 'file') {
26+
fileHandles.push({ handle, nestedPath });
27+
directoryEntryPromises.push(
28+
handle.getFile().then((file: fileType) => {
29+
return {
30+
name: handle.name,
31+
kind: handle.kind,
32+
size: file.size,
33+
type: file.type,
34+
lastModified: file.lastModified,
35+
relativePath: nestedPath
36+
};
37+
})
38+
);
39+
} else if (handle.kind === 'directory') {
40+
directoryHandles.push({ handle, nestedPath });
41+
directoryEntryPromises.push(
42+
(async () => {
43+
return {
44+
name: handle.name,
45+
kind: handle.kind,
46+
relativePath: nestedPath,
47+
entries: await getDirectoryEntriesRecursive(handle, nestedPath)
48+
};
49+
})()
50+
);
51+
}
52+
}
53+
const directoryEntries = await Promise.all(directoryEntryPromises);
54+
directoryEntries.forEach((directoryEntry) => {
55+
entries[directoryEntry.name] = directoryEntry;
56+
});
57+
return entries;
58+
}
59+
60+
function getFileHandle(path: string) {
61+
return fileHandles.find((element) => {
62+
return element.nestedPath === path;
63+
});
64+
}
65+
66+
function getDirectoryHandle(path: string) {
67+
return directoryHandles.find((element) => {
68+
return element.nestedPath === path;
69+
});
70+
}
71+
72+
async function getDirectoryStructure() {
73+
fileHandles = [];
74+
directoryHandles = [];
75+
const root = await navigator.storage.getDirectory();
76+
const structure = await getDirectoryEntriesRecursive(root);
77+
const rootStructure = {
78+
'.': {
79+
kind: 'directory',
80+
relativePath: '.',
81+
entries: structure
82+
}
83+
};
84+
85+
const values = Object.values(rootStructure['.'].entries) as fileType[];
86+
fileList = values.map((file: fileType) => {
87+
return {
88+
name: file.name,
89+
kind: file.kind,
90+
size: file.size,
91+
type: file.type,
92+
lastModified: file.lastModified,
93+
relativePath: file.relativePath
94+
};
95+
});
96+
}
97+
98+
async function saveFile(path: string) {
99+
const fileHandle = getFileHandle(path).handle;
100+
try {
101+
const handle = await window.showSaveFilePicker({
102+
suggestedName: fileHandle.name
103+
});
104+
const writable = await handle.createWritable();
105+
await writable.write(await fileHandle.getFile());
106+
await writable.close();
107+
} catch (error: any) {
108+
if (error.name !== 'AbortError') {
109+
console.error(error.name, error.message);
110+
}
111+
}
112+
}
113+
114+
async function deleteFile(path: string) {
115+
const fileHandle = getFileHandle(path).handle;
116+
try {
117+
await fileHandle.remove();
118+
} catch (error: any) {
119+
console.error(error.name, error.message);
120+
}
121+
getDirectoryStructure();
122+
}
123+
124+
async function deleteDirectory(path: string) {
125+
const directoryHandle = getDirectoryHandle(path)?.handle;
126+
if (!directoryHandle) return;
127+
try {
128+
await directoryHandle.remove({ recursive: true });
129+
} catch (error: any) {
130+
console.error(error.name, error.message);
131+
}
132+
getDirectoryStructure();
133+
}
134+
</script>
135+
136+
<div>
137+
<div class="flex justify-between items-center">
138+
<h2 class="block text-gray-900 font-semibold">OPFS Explorer</h2>
139+
<div>
140+
<button
141+
class=" border border-slate-300 px-2 py-1 rounded-md hover:bg-slate-100 focus:outline-none focus:border-pink-300"
142+
title="Refresh"
143+
on:click={getDirectoryStructure}
144+
><svg
145+
fill="none"
146+
stroke="currentColor"
147+
stroke-width="1.5"
148+
viewBox="0 0 24 24"
149+
xmlns="http://www.w3.org/2000/svg"
150+
aria-hidden="true"
151+
class="w-4 h-4"
152+
>
153+
<path
154+
stroke-linecap="round"
155+
stroke-linejoin="round"
156+
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
157+
/>
158+
</svg></button
159+
>
160+
161+
<!-- <button
162+
class=" border border-slate-300 px-2 py-1 rounded-md hover:bg-slate-100 focus:outline-none focus:border-pink-300"
163+
title="Delete All Files"
164+
on:click={() => deleteDirectory('.')}
165+
><svg
166+
fill="none"
167+
stroke="currentColor"
168+
stroke-width="1.5"
169+
viewBox="0 0 24 24"
170+
xmlns="http://www.w3.org/2000/svg"
171+
aria-hidden="true"
172+
class="h-4 w-4"
173+
>
174+
<path
175+
stroke-linecap="round"
176+
stroke-linejoin="round"
177+
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
178+
/>
179+
</svg></button
180+
> -->
181+
</div>
182+
</div>
183+
{#if fileList.length > 0}
184+
<div class="mt-8 flow-root">
185+
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
186+
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
187+
<table class="min-w-full divide-y divide-gray-300">
188+
<thead>
189+
<tr>
190+
<th class="py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0"
191+
>Path</th
192+
>
193+
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-900">Size</th>
194+
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-900"
195+
>Last Modified</th
196+
>
197+
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-900">Actions</th>
198+
</tr>
199+
</thead>
200+
<tbody class="divide-y divide-gray-200">
201+
{#each fileList as file}
202+
<tr>
203+
<td
204+
class="whitespace-nowrap py-2 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0"
205+
>{file.relativePath}</td
206+
>
207+
<td class="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{file.size}</td>
208+
<td class="whitespace-nowrap px-3 py-2 text-sm text-gray-500"
209+
>{new Date(file.lastModified).toLocaleString()}</td
210+
>
211+
<td class="whitespace-nowrap px-3 py-2 text-sm text-gray-500"
212+
><button
213+
class=" border border-slate-300 px-2 py-1 rounded-md hover:bg-slate-100 focus:outline-none focus:border-pink-300"
214+
title="Download"
215+
on:click={() => saveFile(file.relativePath)}
216+
><svg
217+
fill="none"
218+
stroke="currentColor"
219+
stroke-width="1.5"
220+
viewBox="0 0 24 24"
221+
xmlns="http://www.w3.org/2000/svg"
222+
aria-hidden="true"
223+
class="h-4 w-4"
224+
>
225+
<path
226+
stroke-linecap="round"
227+
stroke-linejoin="round"
228+
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
229+
/>
230+
</svg></button
231+
>
232+
<button
233+
class=" border border-slate-300 px-2 py-1 rounded-md hover:bg-slate-100 focus:outline-none focus:border-pink-300"
234+
title="Delete"
235+
on:click={() => deleteFile(file.relativePath)}
236+
><svg
237+
fill="none"
238+
stroke="currentColor"
239+
stroke-width="1.5"
240+
viewBox="0 0 24 24"
241+
xmlns="http://www.w3.org/2000/svg"
242+
aria-hidden="true"
243+
class="h-4 w-4"
244+
>
245+
<path
246+
stroke-linecap="round"
247+
stroke-linejoin="round"
248+
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
249+
/>
250+
</svg></button
251+
></td
252+
>
253+
</tr>
254+
{/each}
255+
</tbody>
256+
</table>
257+
</div>
258+
</div>
259+
</div>
260+
{/if}
261+
</div>

src/routes/SettingsModal.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import OpfsExplorer from '$lib/components/opfs-explorer.svelte';
23
import Logger, { type LogLevel } from '$lib/logger';
34
import {
45
Dialog,
@@ -48,7 +49,7 @@
4849
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
4950
>
5051
<div
51-
class="relative transform overflow-hidden min-w-[480px] rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6"
52+
class="relative transform overflow-hidden min-w-[700px] rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6"
5253
>
5354
<div class="flex justify-between items-center">
5455
<DialogTitle
@@ -107,6 +108,10 @@
107108
</div>
108109
</fieldset>
109110
</div>
111+
112+
<div class="mt-4">
113+
<OpfsExplorer />
114+
</div>
110115
</div>
111116
</TransitionChild>
112117
</div>

0 commit comments

Comments
 (0)