diff --git a/src/lib/types.ts b/src/lib/types.ts index 7e2cb17c..2680302d 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -4,6 +4,7 @@ * Stored in /users/{uid}/files/{fileID} in Firebase. */ export type UserFile = { + type: string; id: string; // not stored in database; injected by client lastAccessTime: number; title: string; diff --git a/src/routes/(index)/components/Dashboard.svelte b/src/routes/(index)/components/Dashboard.svelte index 9ee6625c..444b28d3 100644 --- a/src/routes/(index)/components/Dashboard.svelte +++ b/src/routes/(index)/components/Dashboard.svelte @@ -1,131 +1,550 @@ -
-
- - Create New File - -
- - {#if firebaseUser.isAnonymous} -
- Not signed in.{' '} - -
- {:else} -
- Signed in as {firebaseUser.displayName}. - -
- {/if} - -
- -

Your Workspaces

-
- -
Show Hidden Files?
- (onUpdateShowHiddenFiles(e))} - /> -
- - {#if filesToShow && filesToShow.length > 0} - - {:else if filesToShow && filesToShow.length === 0} -
No files found. Create a new file above!
- {:else} -
Loading files...
- {/if} +
+
+
+
+ + + + + New File + + + {#if currentFolder || showRecentlyDeleted} + + {/if} + +
+ {#if firebaseUser.isAnonymous} +
+ + + + Not signed in + +
+ {:else} +
+ +
+ + {/if} +
+ + {#if showRecentlyDeleted} +
+ Recently Deleted +
+ {/if} + +
+ +
+
Files
+
+
+ {#if filesToShow && filesToShow.length > 0} + {#each filesToShow as item (item.id)} + + +
handleDragStart(e, item)} + ondragover={item.type === 'folder' ? handleDragOver : undefined} + ondrop={item.type === 'folder' ? (e) => handleDrop(e, item) : undefined} + onclick={(e) => { + e.preventDefault(); + if (item.type === 'folder' && !showRecentlyDeleted) { + openFolder(item); + } else if (item.type === 'file') { + window.location.href = `/${item.id.substring(1)}`; + } + }} + ontouchstart={(e) => handleTouchStart(e, item)} + ontouchmove={handleTouchMove} + ontouchend={(e) => { + e.preventDefault(); + if (item.type === 'folder' && !showRecentlyDeleted) { + openFolder(item); + } else if (item.type === 'file') { + window.location.href = `/${item.id.substring(1)}`; + } + }} + oncontextmenu={(e) => e.preventDefault()} + > + + + +
+
+
+ {#if item.type === 'folder'} + + + + {:else if item.language === 'python' || item.language === 'py'} + + + + + {:else if item.language === 'cpp' || item.language === 'c++'} + + + + {:else if item.language === 'java'} + + + + {:else} + + + + {/if} +
+ {#if renameInput && renameInput.id === item.id} + + { + if (e.key === 'Enter') confirmRename(); + if (e.key === 'Escape') cancelRename(); + }} + onblur={confirmRename} + class="w-full bg-[#404040] text-white px-2 py-1 rounded text-sm border border-gray-500 focus:border-indigo-500 focus:outline-none" + autofocus + /> + {:else} +
{item.name}
+
+
{formatDate(item.lastAccessTime || item.created || Date.now())}
+
+ {/if} +
+
+ + +
+
+
+ {/each} + {:else if filesToShow && filesToShow.length === 0} +
+
+ + + +

{showRecentlyDeleted ? 'No deleted files' : 'No files or folders'}

+ {#if !showRecentlyDeleted} +

Tap the + buttons above to create new items

+ {/if} +
+
+ {:else} +
+
+ Loading... +
+ {/if} +
+
+
+ + {#if $open && actionMenu?.show} +
+ {#if showRecentlyDeleted} +
+ +
+ {:else} +
+ {#if actionMenu.item.type === 'file'} + + {/if} +
+ +
+
+
+
+ +
+
+ {/if} +
+ {/if}
- + \ No newline at end of file diff --git a/src/routes/(index)/components/FilesList.svelte b/src/routes/(index)/components/FilesList.svelte index 4edef6fe..7571a46c 100644 --- a/src/routes/(index)/components/FilesList.svelte +++ b/src/routes/(index)/components/FilesList.svelte @@ -5,9 +5,33 @@ import type { UserFile } from '$lib/types'; import { authState, database } from '$lib/firebase/firebase.svelte'; - import { ref, update } from 'firebase/database'; + import { ref, update, remove, push, set } from 'firebase/database'; - const { files }: { files: UserFile[] } = $props(); + interface FileItem extends UserFile { + name: string; + type: 'file' | 'folder'; + size?: number; + tags?: string[]; + parentFolder?: string | null; + } + + const { files }: { files: FileItem[] } = $props(); + + // Context menu state + let contextMenu: { x: number; y: number; visible: boolean; file?: FileItem } = $state({ + x: 0, + y: 0, + visible: false + }); + + // Modal states + let showDeleteConfirm = $state(false); + let fileToDelete: FileItem | null = $state(null); + let renameInput = $state<{ id: string; value: string } | null>(null); + + // Drag and drop state + let draggedFile: FileItem | null = $state(null); + let selectedItems = $state>(new Set()); function formatCreationTime(creationTime: number): string { if (+dayjs() - +dayjs(creationTime) <= 1000 * 60 * 60 * 24 * 2) { @@ -16,83 +40,414 @@ return dayjs(creationTime).format('MM/DD/YYYY'); } - async function handleToggleHideFile(file: UserFile) { + function formatFileSize(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(1)) + ' ' + sizes[i]; + } + + async function deleteFile(file: FileItem) { + if (!authState.firebaseUser) return; + try { + const fileRef = ref(database, `users/${authState.firebaseUser.uid}/files/${file.id}`); + await remove(fileRef); + showDeleteConfirm = false; + fileToDelete = null; + } catch (error) { + alert('Error deleting file: ' + error); + } + } + + async function createFolder() { + if (!authState.firebaseUser) return; + const name = prompt('Folder name:'); + if (name) { + const folderRef = push(ref(database, `users/${authState.firebaseUser.uid}/files`)); + await set(folderRef, { + name, + title: name, + type: 'folder', + created: Date.now(), + creationTime: Date.now(), + lastAccessTime: Date.now(), + parentFolder: null, + size: 0 + }); + } + hideContextMenu(); + } + + async function createFile() { + if (!authState.firebaseUser) return; + const name = prompt('File name:'); + if (name) { + const fileRef = push(ref(database, `users/${authState.firebaseUser.uid}/files`)); + await set(fileRef, { + name, + title: name, + type: 'file', + created: Date.now(), + creationTime: Date.now(), + lastAccessTime: Date.now(), + parentFolder: null, + size: Math.floor(Math.random() * 100000), + content: '', + language: 'txt' + }); + } + hideContextMenu(); + } + + async function startRename(file: FileItem) { + renameInput = { id: file.id, value: file.title || file.name || '' }; + hideContextMenu(); + } + + async function confirmRename() { + if (renameInput && authState.firebaseUser) { + await update(ref(database, `users/${authState.firebaseUser.uid}/files/${renameInput.id}`), { + title: renameInput.value, + name: renameInput.value + }); + renameInput = null; + } + } + + function cancelRename() { + renameInput = null; + } + + async function addTag(file: FileItem) { + if (!authState.firebaseUser) return; + const tag = prompt('Add tag:'); + if (tag) { + const currentTags = file.tags || []; + await update(ref(database, `users/${authState.firebaseUser.uid}/files/${file.id}`), { + tags: [...currentTags, tag] + }); + } + hideContextMenu(); + } + + async function removeTag(file: FileItem, tagToRemove: string) { if (!authState.firebaseUser) return; - const fileRef = ref(database, `users/${authState.firebaseUser.uid}/files/${file.id}`); - await update(fileRef, { hidden: !file.hidden }); + const updatedTags = (file.tags || []).filter(tag => tag !== tagToRemove); + await update(ref(database, `users/${authState.firebaseUser.uid}/files/${file.id}`), { + tags: updatedTags + }); } function formatLanguage(language: string | null): string { if (language == 'py') return 'Python'; if (language == 'java') return 'Java'; if (language == 'cpp') return 'C++'; - return 'Unknown'; + if (language == 'js') return 'JavaScript'; + if (language == 'ts') return 'TypeScript'; + if (language == 'html') return 'HTML'; + if (language == 'css') return 'CSS'; + if (language == 'json') return 'JSON'; + return language || 'Text'; + } + + function getFileIcon(file: FileItem): string { + if (file.type === 'folder') { + return '📁'; + } + const lang = file.language?.toLowerCase(); + if (lang === 'py') return '🐍'; + if (lang === 'java') return '☕'; + if (lang === 'cpp' || lang === 'c++') return '⚙️'; + if (lang === 'js') return '🟨'; + if (lang === 'ts') return '🔷'; + if (lang === 'html') return '🌐'; + if (lang === 'css') return '🎨'; + if (lang === 'json') return '📋'; + return '📄'; } + + // Context menu handlers + const handleRightClick = (e: MouseEvent, file?: FileItem) => { + e.preventDefault(); + contextMenu = { + x: e.clientX, + y: e.clientY, + visible: true, + file + }; + }; + + const hideContextMenu = () => { + contextMenu.visible = false; + }; + + // Drag and drop handlers + const handleDragStart = (e: DragEvent, file: FileItem) => { + draggedFile = file; + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', file.id); + } + }; + + const handleDragEnd = () => { + draggedFile = null; + }; + + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + e.dataTransfer!.dropEffect = 'move'; + }; + + const handleDrop = async (e: DragEvent, targetFolder: FileItem) => { + e.preventDefault(); + if (draggedFile && targetFolder.type === 'folder' && draggedFile.id !== targetFolder.id && authState.firebaseUser) { + await update(ref(database, `users/${authState.firebaseUser.uid}/files/${draggedFile.id}`), { + parentFolder: targetFolder.id + }); + } + draggedFile = null; + }; + + const openFolder = (folder: FileItem) => { + // This would need to be handled by the parent component + // For now, we'll just log it + console.log('Opening folder:', folder.name); + }; + + // Close context menu on outside click + $effect(() => { + const handleClick = () => { + hideContextMenu(); + }; + + document.addEventListener('click', handleClick); + return () => document.removeEventListener('click', handleClick); + }); -
-
-
- - - - {#each ['Name', 'Last Accessed', 'Created', 'Language'] as col, i (col)} - - {/each} - - - - - {#each files as file (file.id)} - - - - - - - - {/each} - -
0 && 'px-2', - 'py-3.5 text-left text-sm font-semibold text-black dark:text-gray-100' - ]} - > - {col} - - Actions -
- {#if file.hidden} - - {file.title || '(Unnamed File)'} (Hidden) - - {:else} - - {file.title || '(Unnamed File)'} - - {/if} - - {formatCreationTime(file.lastAccessTime)} - - {file.creationTime ? formatCreationTime(file.creationTime) : 'Unknown'} - - {file.language ? formatLanguage(file.language) : 'Unknown'} - +
+ +
+
Name
+
Date Modified
+
Size
+
Tags
+
+ + + + +
handleRightClick(e)} + > + {#if files && files.length > 0} + {#each files as file (file.id)} +
handleDragStart(e, file)} + ondragend={handleDragEnd} + ondragover={file.type === 'folder' ? handleDragOver : undefined} + ondrop={file.type === 'folder' ? (e) => handleDrop(e, file) : undefined} + oncontextmenu={(e) => handleRightClick(e, file)} + ondblclick={() => file.type === 'folder' ? openFolder(file) : (window.location.href = `/${file.id.substring(1)}`)} + > + +
+
+ {getFileIcon(file)} +
+ + {#if renameInput && renameInput.id === file.id} + + { + if (e.key === 'Enter') confirmRename(); + if (e.key === 'Escape') cancelRename(); + }} + onblur={confirmRename} + class="bg-[#404040] text-white px-2 py-1 rounded text-sm border border-gray-500 focus:border-indigo-500 focus:outline-none" + autofocus + /> + {:else} + + {file.title || file.name || '(Unnamed)'} + + {/if} +
+ + +
+ {formatCreationTime(file.lastAccessTime || file.creationTime || Date.now())} +
+ + +
+ {file.type === 'folder' ? '--' : formatFileSize(file.size || 0)} +
+ + +
+ {#each (file.tags || []) as tag} + + + {tag} + -
-
+ + {/each} +
+
+ {/each} + {:else} +
+
+ + + +

No files or folders

+

Right-click to create new items

+
+
+ {/if}
+ + +{#if contextMenu.visible} + + +
e.stopPropagation()} + > + {#if contextMenu.file} + + + + + + + +
+ + + {:else} + + + + {/if} +
+{/if} + + +{#if showDeleteConfirm && fileToDelete} +
+
+

Delete {fileToDelete.type === 'folder' ? 'Folder' : 'File'}

+

+ Are you sure you want to delete "{fileToDelete.title || fileToDelete.name || '(Unnamed)'}"? + {#if fileToDelete.type === 'folder'} + This will also delete all files inside this folder. + {/if} + This action cannot be undone. +

+
+ + +
+
+
+{/if} \ No newline at end of file diff --git a/src/routes/new/+page.server.ts b/src/routes/new/+page.server.ts index c778ce11..c46f7a2b 100644 --- a/src/routes/new/+page.server.ts +++ b/src/routes/new/+page.server.ts @@ -18,6 +18,7 @@ export const actions = { const language = data.get('language') as string; const compilerOptions = data.get('compilerOptions') as string; const filename = data.get('filename') as string; + const kind = 'file'; if (!username || !userID || !defaultPermission || !language || compilerOptions === null) { return fail(400, { @@ -28,6 +29,7 @@ export const actions = { const resp = await getDatabase(firebaseApp) .ref('/files') .push({ + kind: kind, users: { [userID]: { name: username,