♻️ Makes use of tailwind for styling
This commit is contained in:
@@ -1,374 +1,227 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fileSystemClient, type TransferProgress } from '$lib/filesystem/chunkedTransfer'
|
import { fileSystemClient, type TransferProgress } from '$lib/filesystem/chunkedTransfer'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
let currentPath = '/'
|
let currentPath = '/'
|
||||||
let files: Array<{ name: string; size: number }> = []
|
let files: Array<{ name: string; size: number }> = []
|
||||||
let directories: Array<{ name: string }> = []
|
let directories: Array<{ name: string }> = []
|
||||||
let loading = false
|
let loading = false
|
||||||
let error = ''
|
let error = ''
|
||||||
let uploadProgress: TransferProgress | null = null
|
let uploadProgress: TransferProgress | null = null
|
||||||
let downloadProgress: TransferProgress | null = null
|
let downloadProgress: TransferProgress | null = null
|
||||||
|
|
||||||
async function loadDirectory() {
|
const joinPath = (name: string) => (currentPath === '/' ? '/' + name : currentPath + '/' + name)
|
||||||
loading = true
|
const getError = (e: unknown, fallback: string) =>
|
||||||
error = ''
|
e instanceof Error ? e.message : (e as { error?: string })?.error || fallback
|
||||||
try {
|
|
||||||
const result = await fileSystemClient.listDirectory(currentPath)
|
|
||||||
if (result.success) {
|
|
||||||
files = result.files
|
|
||||||
directories = result.directories
|
|
||||||
} else {
|
|
||||||
error = result.error || 'Failed to load directory'
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
error = e instanceof Error ? e.message : 'Unknown error'
|
|
||||||
} finally {
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function navigateTo(path: string) {
|
async function loadDirectory() {
|
||||||
currentPath = path
|
loading = true
|
||||||
await loadDirectory()
|
error = ''
|
||||||
}
|
try {
|
||||||
|
const result = await fileSystemClient.listDirectory(currentPath)
|
||||||
|
if (result.success) {
|
||||||
|
files = result.files
|
||||||
|
directories = result.directories
|
||||||
|
} else {
|
||||||
|
error = result.error || 'Failed to load directory'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = getError(e, 'Unknown error')
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleFileUpload(event: Event) {
|
async function navigateTo(path: string) {
|
||||||
const input = event.target as HTMLInputElement
|
currentPath = path
|
||||||
const file = input.files?.[0]
|
await loadDirectory()
|
||||||
if (!file) return
|
}
|
||||||
|
|
||||||
uploadProgress = null
|
async function handleFileUpload(event: Event) {
|
||||||
error = ''
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
const destinationPath = currentPath.endsWith('/')
|
uploadProgress = null
|
||||||
? currentPath + file.name
|
error = ''
|
||||||
: currentPath + '/' + file.name
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fileSystemClient.uploadFileFromBrowser(
|
const result = await fileSystemClient.uploadFileFromBrowser(
|
||||||
destinationPath,
|
joinPath(file.name),
|
||||||
file,
|
file,
|
||||||
(progress) => {
|
p => (uploadProgress = p)
|
||||||
uploadProgress = progress
|
)
|
||||||
}
|
if (result.success) await loadDirectory()
|
||||||
)
|
else error = result.error || 'Upload failed'
|
||||||
|
} catch (e) {
|
||||||
|
error = getError(e, 'Upload error')
|
||||||
|
} finally {
|
||||||
|
uploadProgress = null
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result.success) {
|
async function handleDownload(filename: string) {
|
||||||
await loadDirectory()
|
downloadProgress = null
|
||||||
} else {
|
error = ''
|
||||||
error = result.error || 'Upload failed'
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
error = e instanceof Error ? e.message : 'Upload error'
|
|
||||||
} finally {
|
|
||||||
uploadProgress = null
|
|
||||||
input.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDownload(filename: string) {
|
try {
|
||||||
downloadProgress = null
|
const result = await fileSystemClient.downloadFileAndSave(
|
||||||
error = ''
|
joinPath(filename),
|
||||||
|
filename,
|
||||||
|
p => (downloadProgress = p)
|
||||||
|
)
|
||||||
|
if (!result.success) error = result.error || 'Download failed'
|
||||||
|
} catch (e) {
|
||||||
|
error = getError(e, 'Download error')
|
||||||
|
} finally {
|
||||||
|
downloadProgress = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filePath = currentPath.endsWith('/')
|
async function handleDelete(name: string, isDirectory: boolean) {
|
||||||
? currentPath + filename
|
if (!confirm(`Delete ${isDirectory ? 'directory' : 'file'} "${name}"?`)) return
|
||||||
: currentPath + '/' + filename
|
error = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fileSystemClient.downloadFileAndSave(filePath, filename, (progress) => {
|
const result = await fileSystemClient.deleteFile(joinPath(name))
|
||||||
downloadProgress = progress
|
if (result.success) await loadDirectory()
|
||||||
})
|
else error = result.error || 'Delete failed'
|
||||||
|
} catch (e) {
|
||||||
|
error = getError(e, 'Delete error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.success) {
|
async function handleCreateDirectory() {
|
||||||
error = result.error || 'Download failed'
|
const name = prompt('Enter directory name:')
|
||||||
}
|
if (!name) return
|
||||||
} catch (e) {
|
error = ''
|
||||||
error = e instanceof Error ? e.message : 'Download error'
|
|
||||||
} finally {
|
|
||||||
downloadProgress = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDelete(name: string, isDirectory: boolean) {
|
try {
|
||||||
if (!confirm(`Delete ${isDirectory ? 'directory' : 'file'} "${name}"?`)) return
|
const result = await fileSystemClient.createDirectory(joinPath(name))
|
||||||
|
if (result.success) await loadDirectory()
|
||||||
|
else error = result.error || 'Failed to create directory'
|
||||||
|
} catch (e) {
|
||||||
|
error = getError(e, 'Error creating directory')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
error = ''
|
function formatBytes(bytes: number): string {
|
||||||
const path = currentPath.endsWith('/') ? currentPath + name : currentPath + '/' + name
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
onMount(loadDirectory)
|
||||||
const result = await fileSystemClient.deleteFile(path)
|
|
||||||
if (result.success) {
|
|
||||||
await loadDirectory()
|
|
||||||
} else {
|
|
||||||
error = result.error || 'Delete failed'
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
error = e instanceof Error ? e.message : 'Delete error'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCreateDirectory() {
|
|
||||||
const name = prompt('Enter directory name:')
|
|
||||||
if (!name) return
|
|
||||||
|
|
||||||
error = ''
|
|
||||||
const path = currentPath.endsWith('/') ? currentPath + name : currentPath + '/' + name
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await fileSystemClient.createDirectory(path)
|
|
||||||
if (result.success) {
|
|
||||||
await loadDirectory()
|
|
||||||
} else {
|
|
||||||
error = result.error || 'Failed to create directory'
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
error = e instanceof Error ? e.message : 'Error creating directory'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
|
||||||
if (bytes === 0) return '0 B'
|
|
||||||
const k = 1024
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
loadDirectory()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="file-manager">
|
<div class="max-w-3xl mx-auto my-8 p-4 border border-gray-300 rounded-lg bg-white">
|
||||||
<div class="toolbar">
|
<div class="mb-4">
|
||||||
<h2>File Manager</h2>
|
<h2 class="m-0 mb-2">File Manager</h2>
|
||||||
<div class="path">Current: {currentPath}</div>
|
<div class="font-mono bg-gray-100 p-2 rounded mb-2">Current: {currentPath}</div>
|
||||||
<div class="actions">
|
<div class="flex gap-2">
|
||||||
<button on:click={handleCreateDirectory}>New Folder</button>
|
<button
|
||||||
<label class="upload-btn">
|
class="px-4 py-2 bg-blue-600 text-white rounded cursor-pointer hover:bg-blue-700"
|
||||||
Upload File
|
on:click={handleCreateDirectory}>New Folder</button
|
||||||
<input type="file" on:change={handleFileUpload} style="display: none;" />
|
>
|
||||||
</label>
|
<label
|
||||||
<button on:click={loadDirectory}>Refresh</button>
|
class="px-4 py-2 bg-blue-600 text-white rounded cursor-pointer hover:bg-blue-700"
|
||||||
</div>
|
>
|
||||||
</div>
|
Upload File
|
||||||
|
<input type="file" on:change={handleFileUpload} class="hidden" />
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white rounded cursor-pointer hover:bg-blue-700"
|
||||||
|
on:click={loadDirectory}>Refresh</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="error">{error}</div>
|
<div class="bg-red-100 text-red-800 p-3 rounded mb-4">{error}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if uploadProgress}
|
{#if uploadProgress}
|
||||||
<div class="progress">
|
<div class="mb-4">
|
||||||
<div class="progress-label">
|
<div class="mb-2 text-sm">
|
||||||
Uploading: {uploadProgress.percentage.toFixed(1)}% ({formatBytes(
|
Uploading: {uploadProgress.percentage.toFixed(1)}% ({formatBytes(
|
||||||
uploadProgress.bytesTransferred
|
uploadProgress.bytesTransferred
|
||||||
)} / {formatBytes(uploadProgress.totalBytes)})
|
)} / {formatBytes(uploadProgress.totalBytes)})
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar">
|
<div class="h-5 bg-gray-200 rounded overflow-hidden">
|
||||||
<div class="progress-fill" style="width: {uploadProgress.percentage}%"></div>
|
<div
|
||||||
</div>
|
class="h-full bg-green-600 transition-all duration-300"
|
||||||
</div>
|
style="width: {uploadProgress.percentage}%"
|
||||||
{/if}
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if downloadProgress}
|
{#if downloadProgress}
|
||||||
<div class="progress">
|
<div class="mb-4">
|
||||||
<div class="progress-label">
|
<div class="mb-2 text-sm">
|
||||||
Downloading: {downloadProgress.percentage.toFixed(1)}% ({formatBytes(
|
Downloading: {downloadProgress.percentage.toFixed(1)}% ({formatBytes(
|
||||||
downloadProgress.bytesTransferred
|
downloadProgress.bytesTransferred
|
||||||
)} / {formatBytes(downloadProgress.totalBytes)})
|
)} / {formatBytes(downloadProgress.totalBytes)})
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar">
|
<div class="h-5 bg-gray-200 rounded overflow-hidden">
|
||||||
<div class="progress-fill" style="width: {downloadProgress.percentage}%"></div>
|
<div
|
||||||
</div>
|
class="h-full bg-green-600 transition-all duration-300"
|
||||||
</div>
|
style="width: {downloadProgress.percentage}%"
|
||||||
{/if}
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="file-list">
|
<div class="border border-gray-300 rounded min-h-[200px]">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="loading">Loading...</div>
|
<div class="text-center p-8 text-gray-500">Loading...</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#if currentPath !== '/'}
|
{#if currentPath !== '/'}
|
||||||
<div class="file-item directory" on:click={() => navigateTo('/')}>
|
<div
|
||||||
<span class="icon">📁</span>
|
class="flex items-center p-3 border-b border-gray-100 gap-2 bg-gray-50 cursor-pointer"
|
||||||
<span class="name">..</span>
|
on:click={() => navigateTo('/')}
|
||||||
</div>
|
>
|
||||||
{/if}
|
<span class="text-2xl">📁</span>
|
||||||
|
<span class="flex-1 hover:underline">..</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#each directories as dir}
|
{#each directories as dir}
|
||||||
<div class="file-item directory">
|
<div class="flex items-center p-3 border-b border-gray-100 gap-2 bg-gray-50">
|
||||||
<span class="icon">📁</span>
|
<span class="text-2xl">📁</span>
|
||||||
<span class="name" on:click={() => navigateTo(currentPath + '/' + dir.name)}
|
<span
|
||||||
>{dir.name}</span
|
class="flex-1 cursor-pointer hover:underline"
|
||||||
>
|
on:click={() => navigateTo(currentPath + '/' + dir.name)}>{dir.name}</span
|
||||||
<button class="delete-btn" on:click={() => handleDelete(dir.name, true)}>Delete</button>
|
>
|
||||||
</div>
|
<button
|
||||||
{/each}
|
class="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700"
|
||||||
|
on:click={() => handleDelete(dir.name, true)}>Delete</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
{#each files as file}
|
{#each files as file}
|
||||||
<div class="file-item">
|
<div class="flex items-center p-3 border-b border-gray-100 gap-2 last:border-b-0">
|
||||||
<span class="icon">📄</span>
|
<span class="text-2xl">📄</span>
|
||||||
<span class="name">{file.name}</span>
|
<span class="flex-1">{file.name}</span>
|
||||||
<span class="size">{formatBytes(file.size)}</span>
|
<span class="text-gray-500 text-sm">{formatBytes(file.size)}</span>
|
||||||
<button class="download-btn" on:click={() => handleDownload(file.name)}>Download</button>
|
<button
|
||||||
<button class="delete-btn" on:click={() => handleDelete(file.name, false)}>Delete</button>
|
class="px-3 py-1 bg-green-600 text-white rounded text-sm hover:bg-green-700"
|
||||||
</div>
|
on:click={() => handleDownload(file.name)}>Download</button
|
||||||
{/each}
|
>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700"
|
||||||
|
on:click={() => handleDelete(file.name, false)}>Delete</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
{#if files.length === 0 && directories.length === 0}
|
{#if files.length === 0 && directories.length === 0}
|
||||||
<div class="empty">Directory is empty</div>
|
<div class="text-center p-8 text-gray-500">Directory is empty</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.file-manager {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 2rem auto;
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar h2 {
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.path {
|
|
||||||
font-family: monospace;
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions button,
|
|
||||||
.upload-btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions button:hover,
|
|
||||||
.upload-btn:hover {
|
|
||||||
background: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-label {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
height: 20px;
|
|
||||||
background: #e9ecef;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: #28a745;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item.directory {
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
flex: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size {
|
|
||||||
color: #6c757d;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn,
|
|
||||||
.delete-btn {
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn {
|
|
||||||
background: #28a745;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn:hover {
|
|
||||||
background: #218838;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-btn {
|
|
||||||
background: #dc3545;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-btn:hover {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading,
|
|
||||||
.empty {
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user