Spaces:
Sleeping
Sleeping
Commit ·
61fcdc2
1
Parent(s): b1df25f
Fix page refresh on navigation and folder clicks - use button elements and prevent default
Browse files- .dockerignore +0 -16
- Dockerfile +0 -28
- README.md +3 -1
- app.js +87 -37
- index.html +23 -23
- js/api/hfService.js +41 -40
- js/main.js +7 -3
- js/ui/uiRenderer.js +2 -0
- server/app.py +1 -0
.dockerignore
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
.git
|
| 2 |
-
.gitignore
|
| 3 |
-
.DS_Store
|
| 4 |
-
__pycache__
|
| 5 |
-
*.pyc
|
| 6 |
-
.pytest_cache
|
| 7 |
-
.env
|
| 8 |
-
venv/
|
| 9 |
-
env/
|
| 10 |
-
logs/
|
| 11 |
-
data/
|
| 12 |
-
*.log
|
| 13 |
-
.vscode
|
| 14 |
-
.idea
|
| 15 |
-
node_modules
|
| 16 |
-
.env.local
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
# Use official Python runtime as a parent image
|
| 2 |
-
FROM python:3.10-slim
|
| 3 |
-
|
| 4 |
-
# Set working directory in container
|
| 5 |
-
WORKDIR /app
|
| 6 |
-
|
| 7 |
-
# Copy requirements first for better caching
|
| 8 |
-
COPY server/requirements.txt .
|
| 9 |
-
|
| 10 |
-
# Install Python dependencies
|
| 11 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
-
|
| 13 |
-
# Copy the entire project
|
| 14 |
-
COPY . .
|
| 15 |
-
|
| 16 |
-
# Create data and logs directories
|
| 17 |
-
RUN mkdir -p data logs
|
| 18 |
-
|
| 19 |
-
# Set environment variables
|
| 20 |
-
ENV FLASK_APP=server/app.py
|
| 21 |
-
ENV FLASK_ENV=production
|
| 22 |
-
ENV PYTHONUNBUFFERED=1
|
| 23 |
-
|
| 24 |
-
# Expose port 5000 for Flask
|
| 25 |
-
EXPOSE 5000
|
| 26 |
-
|
| 27 |
-
# Run the Flask application
|
| 28 |
-
CMD ["python", "-m", "flask", "run", "--host", "0.0.0.0"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -3,7 +3,9 @@ title: DocVault App
|
|
| 3 |
emoji: 📁
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
-
sdk:
|
|
|
|
|
|
|
| 7 |
---
|
| 8 |
|
| 9 |
# DocVault - Offline-First Document Storage System
|
|
|
|
| 3 |
emoji: 📁
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
+
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
+
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
# DocVault - Offline-First Document Storage System
|
app.js
CHANGED
|
@@ -1,19 +1,7 @@
|
|
| 1 |
// DocVault — Offline-First Document Storage System
|
| 2 |
// Uses local Flask backend for all operations
|
| 3 |
|
| 4 |
-
|
| 5 |
-
const API_BASE = (() => {
|
| 6 |
-
const host = window.location.hostname;
|
| 7 |
-
const isLocal = ['localhost', '127.0.0.1'].includes(host);
|
| 8 |
-
|
| 9 |
-
if (isLocal) {
|
| 10 |
-
return 'http://localhost:5000/api';
|
| 11 |
-
} else {
|
| 12 |
-
// For HF Spaces and other deployments, use root path
|
| 13 |
-
return '/api';
|
| 14 |
-
}
|
| 15 |
-
})();
|
| 16 |
-
|
| 17 |
const USER_ID = 'default_user';
|
| 18 |
const DEFAULT_FOLDER = '';
|
| 19 |
|
|
@@ -36,17 +24,26 @@ async function apiFetch(endpoint, options = {}) {
|
|
| 36 |
async function listFilesAPI(path = DEFAULT_FOLDER) {
|
| 37 |
try {
|
| 38 |
const queryPath = path ? `?folder_path=${encodeURIComponent(path)}` : '';
|
|
|
|
|
|
|
| 39 |
const res = await apiFetch(`/list${queryPath}`);
|
| 40 |
|
|
|
|
|
|
|
| 41 |
if (!res.ok) {
|
| 42 |
-
if (res.status === 404)
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
const data = await res.json();
|
|
|
|
|
|
|
| 47 |
if (!data.success) {
|
| 48 |
console.error('API error:', data.error);
|
| 49 |
-
|
| 50 |
}
|
| 51 |
|
| 52 |
const files = (data.files || []).map(f => ({
|
|
@@ -66,9 +63,11 @@ async function listFilesAPI(path = DEFAULT_FOLDER) {
|
|
| 66 |
modified_at: f.modified_at
|
| 67 |
}));
|
| 68 |
|
|
|
|
| 69 |
return { files, folders };
|
| 70 |
} catch (err) {
|
| 71 |
console.error('List files error:', err);
|
|
|
|
| 72 |
return { files: [], folders: [] };
|
| 73 |
}
|
| 74 |
}
|
|
@@ -79,23 +78,32 @@ async function uploadFileAPI(file, destPath) {
|
|
| 79 |
const fileBlob = file instanceof File ? file : file.content;
|
| 80 |
const filename = file instanceof File ? file.name : (file.name || 'upload.bin');
|
| 81 |
|
|
|
|
|
|
|
| 82 |
const formData = new FormData();
|
| 83 |
formData.append('folder_path', folderPath);
|
| 84 |
formData.append('file', fileBlob, filename);
|
| 85 |
|
| 86 |
-
const
|
|
|
|
|
|
|
|
|
|
| 87 |
method: 'POST',
|
| 88 |
headers: { 'X-User-ID': USER_ID },
|
| 89 |
body: formData
|
| 90 |
});
|
| 91 |
|
|
|
|
|
|
|
| 92 |
if (!res.ok) {
|
| 93 |
-
const errData = await res.json();
|
| 94 |
-
throw new Error(errData.error || `Upload failed: ${res.status}`);
|
| 95 |
}
|
| 96 |
|
| 97 |
const data = await res.json();
|
| 98 |
-
|
|
|
|
|
|
|
| 99 |
|
| 100 |
return data;
|
| 101 |
} catch (err) {
|
|
@@ -362,6 +370,7 @@ function showSkeletons(container, count = 6) {
|
|
| 362 |
// ─── FETCH FILES ──────────────────────────────────────────
|
| 363 |
async function fetchAndRender() {
|
| 364 |
if (isFetching) {
|
|
|
|
| 365 |
return;
|
| 366 |
}
|
| 367 |
isFetching = true;
|
|
@@ -372,11 +381,16 @@ async function fetchAndRender() {
|
|
| 372 |
showSkeletons(filesContainer, 6);
|
| 373 |
try {
|
| 374 |
const prefix = getFolderPath();
|
|
|
|
| 375 |
const pathSnapshot = JSON.stringify(currentPath); // Capture for safety check
|
| 376 |
const { files, folders } = await listFilesAPI(prefix);
|
| 377 |
|
|
|
|
|
|
|
| 378 |
// SAFETY CHECK: Path may have changed due to user clicking elsewhere
|
| 379 |
if (JSON.stringify(currentPath) !== pathSnapshot) {
|
|
|
|
|
|
|
| 380 |
return;
|
| 381 |
}
|
| 382 |
|
|
@@ -504,26 +518,23 @@ function renderFolders(folders) {
|
|
| 504 |
</div>
|
| 505 |
</div>`;
|
| 506 |
|
| 507 |
-
//
|
| 508 |
-
|
|
|
|
|
|
|
|
|
|
| 509 |
// Don't navigate if clicking on the actions menu
|
| 510 |
if (e.target.closest('.folder-actions')) {
|
| 511 |
return;
|
| 512 |
}
|
| 513 |
|
| 514 |
-
// Stop all propagation for actual navigation clicks
|
| 515 |
-
e.preventDefault();
|
| 516 |
-
e.stopPropagation();
|
| 517 |
-
e.stopImmediatePropagation();
|
| 518 |
-
|
| 519 |
// Navigate into the folder
|
| 520 |
-
|
| 521 |
-
|
|
|
|
|
|
|
| 522 |
fetchAndRender();
|
| 523 |
-
|
| 524 |
-
};
|
| 525 |
-
|
| 526 |
-
card.addEventListener('click', handleFolderClick, true); // Use capture phase
|
| 527 |
|
| 528 |
// Attach menu functionality
|
| 529 |
attachCardMenu(card, folder.path, 'folder');
|
|
@@ -860,7 +871,10 @@ function hideProgress() { uploadProgress.classList.remove('active'); }
|
|
| 860 |
|
| 861 |
// ─── FILE INPUT ───────────────────────────────────────────
|
| 862 |
fileInput.addEventListener('change', () => {
|
| 863 |
-
|
|
|
|
|
|
|
|
|
|
| 864 |
fileInput.value = '';
|
| 865 |
});
|
| 866 |
|
|
@@ -877,9 +891,24 @@ createFolderBtn.addEventListener('click', () => {
|
|
| 877 |
createFolderModal.classList.add('active');
|
| 878 |
setTimeout(() => folderNameInput.focus(), 100);
|
| 879 |
});
|
| 880 |
-
uploadFileBtn.addEventListener('click', () => {
|
|
|
|
|
|
|
|
|
|
| 881 |
newDropdown.classList.remove('active');
|
| 882 |
-
fileInput
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
});
|
| 884 |
|
| 885 |
// ─── CREATE FOLDER ────────────────────────────────────────
|
|
@@ -921,26 +950,37 @@ function setNavActive(nav) {
|
|
| 921 |
}
|
| 922 |
|
| 923 |
navMyFiles.addEventListener('click', (e) => {
|
|
|
|
| 924 |
e.preventDefault();
|
| 925 |
e.stopPropagation();
|
| 926 |
e.stopImmediatePropagation();
|
| 927 |
-
currentBrowse = 'files';
|
|
|
|
| 928 |
setNavActive(navMyFiles);
|
| 929 |
fetchAndRender();
|
|
|
|
| 930 |
});
|
| 931 |
|
| 932 |
navRecent.addEventListener('click', (e) => {
|
|
|
|
| 933 |
e.preventDefault();
|
|
|
|
|
|
|
| 934 |
currentBrowse = 'recent';
|
| 935 |
setNavActive(navRecent);
|
| 936 |
renderRecentView();
|
|
|
|
| 937 |
});
|
| 938 |
|
| 939 |
navStarred.addEventListener('click', (e) => {
|
|
|
|
| 940 |
e.preventDefault();
|
|
|
|
|
|
|
| 941 |
currentBrowse = 'starred';
|
| 942 |
setNavActive(navStarred);
|
| 943 |
renderStarredView();
|
|
|
|
| 944 |
});
|
| 945 |
|
| 946 |
function renderRecentView() {
|
|
@@ -1080,6 +1120,16 @@ confirmRenameBtn.addEventListener('click', async () => {
|
|
| 1080 |
// ─── INIT ─────────────────────────────────────────────────
|
| 1081 |
// Initialize offline-first DocVault
|
| 1082 |
(function initApp() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1083 |
// Show welcome message
|
| 1084 |
showToast('🎉 Welcome to DocVault! Loading your files...', 'info');
|
| 1085 |
|
|
|
|
| 1 |
// DocVault — Offline-First Document Storage System
|
| 2 |
// Uses local Flask backend for all operations
|
| 3 |
|
| 4 |
+
const API_BASE = 'http://localhost:5000/api';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
const USER_ID = 'default_user';
|
| 6 |
const DEFAULT_FOLDER = '';
|
| 7 |
|
|
|
|
| 24 |
async function listFilesAPI(path = DEFAULT_FOLDER) {
|
| 25 |
try {
|
| 26 |
const queryPath = path ? `?folder_path=${encodeURIComponent(path)}` : '';
|
| 27 |
+
const url = `${API_BASE}/list${queryPath}`;
|
| 28 |
+
console.log('Fetching from URL:', url); // Debug
|
| 29 |
const res = await apiFetch(`/list${queryPath}`);
|
| 30 |
|
| 31 |
+
console.log('API Response status:', res.status); // Debug
|
| 32 |
+
|
| 33 |
if (!res.ok) {
|
| 34 |
+
if (res.status === 404) {
|
| 35 |
+
console.warn('Path not found, returning empty list'); // Debug
|
| 36 |
+
return { files: [], folders: [] };
|
| 37 |
+
}
|
| 38 |
+
throw new Error(`Failed to list files: ${res.status} ${res.statusText}`);
|
| 39 |
}
|
| 40 |
|
| 41 |
const data = await res.json();
|
| 42 |
+
console.log('API Data:', data); // Debug
|
| 43 |
+
|
| 44 |
if (!data.success) {
|
| 45 |
console.error('API error:', data.error);
|
| 46 |
+
throw new Error(data.error || 'API returned success: false');
|
| 47 |
}
|
| 48 |
|
| 49 |
const files = (data.files || []).map(f => ({
|
|
|
|
| 63 |
modified_at: f.modified_at
|
| 64 |
}));
|
| 65 |
|
| 66 |
+
console.log('Parsed folders:', folders.length, 'files:', files.length); // Debug
|
| 67 |
return { files, folders };
|
| 68 |
} catch (err) {
|
| 69 |
console.error('List files error:', err);
|
| 70 |
+
showToast(`Error loading files: ${err.message}`, 'error');
|
| 71 |
return { files: [], folders: [] };
|
| 72 |
}
|
| 73 |
}
|
|
|
|
| 78 |
const fileBlob = file instanceof File ? file : file.content;
|
| 79 |
const filename = file instanceof File ? file.name : (file.name || 'upload.bin');
|
| 80 |
|
| 81 |
+
console.log('Uploading file:', filename, 'to:', folderPath); // Debug
|
| 82 |
+
|
| 83 |
const formData = new FormData();
|
| 84 |
formData.append('folder_path', folderPath);
|
| 85 |
formData.append('file', fileBlob, filename);
|
| 86 |
|
| 87 |
+
const url = `${API_BASE}/upload-file`;
|
| 88 |
+
console.log('Upload endpoint:', url); // Debug
|
| 89 |
+
|
| 90 |
+
const res = await fetch(url, {
|
| 91 |
method: 'POST',
|
| 92 |
headers: { 'X-User-ID': USER_ID },
|
| 93 |
body: formData
|
| 94 |
});
|
| 95 |
|
| 96 |
+
console.log('Upload response status:', res.status); // Debug
|
| 97 |
+
|
| 98 |
if (!res.ok) {
|
| 99 |
+
const errData = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
| 100 |
+
throw new Error(errData.error || `Upload failed: ${res.status} ${res.statusText}`);
|
| 101 |
}
|
| 102 |
|
| 103 |
const data = await res.json();
|
| 104 |
+
console.log('Upload API response:', data); // Debug
|
| 105 |
+
|
| 106 |
+
if (!data.success) throw new Error(data.error || 'Upload API returned success: false');
|
| 107 |
|
| 108 |
return data;
|
| 109 |
} catch (err) {
|
|
|
|
| 370 |
// ─── FETCH FILES ──────────────────────────────────────────
|
| 371 |
async function fetchAndRender() {
|
| 372 |
if (isFetching) {
|
| 373 |
+
console.warn('Already fetching, ignoring request');
|
| 374 |
return;
|
| 375 |
}
|
| 376 |
isFetching = true;
|
|
|
|
| 381 |
showSkeletons(filesContainer, 6);
|
| 382 |
try {
|
| 383 |
const prefix = getFolderPath();
|
| 384 |
+
console.log('Fetching contents for path:', prefix); // Debug
|
| 385 |
const pathSnapshot = JSON.stringify(currentPath); // Capture for safety check
|
| 386 |
const { files, folders } = await listFilesAPI(prefix);
|
| 387 |
|
| 388 |
+
console.log('API returned:', { folders: folders.length, files: files.length }); // Debug
|
| 389 |
+
|
| 390 |
// SAFETY CHECK: Path may have changed due to user clicking elsewhere
|
| 391 |
if (JSON.stringify(currentPath) !== pathSnapshot) {
|
| 392 |
+
console.log('Path changed during fetch'); // Debug
|
| 393 |
+
isFetching = false;
|
| 394 |
return;
|
| 395 |
}
|
| 396 |
|
|
|
|
| 518 |
</div>
|
| 519 |
</div>`;
|
| 520 |
|
| 521 |
+
// FIXED: Simplified folder click handler
|
| 522 |
+
card.addEventListener('click', function(e) {
|
| 523 |
+
e.preventDefault();
|
| 524 |
+
e.stopPropagation();
|
| 525 |
+
|
| 526 |
// Don't navigate if clicking on the actions menu
|
| 527 |
if (e.target.closest('.folder-actions')) {
|
| 528 |
return;
|
| 529 |
}
|
| 530 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
// Navigate into the folder
|
| 532 |
+
const folderName = folder.path.split('/').pop();
|
| 533 |
+
console.log('Navigating to folder:', folderName, 'Full path:', folder.path); // Debug
|
| 534 |
+
currentPath.push(folderName);
|
| 535 |
+
addToRecent(folder.path, folderName, 'folder');
|
| 536 |
fetchAndRender();
|
| 537 |
+
});
|
|
|
|
|
|
|
|
|
|
| 538 |
|
| 539 |
// Attach menu functionality
|
| 540 |
attachCardMenu(card, folder.path, 'folder');
|
|
|
|
| 871 |
|
| 872 |
// ─── FILE INPUT ───────────────────────────────────────────
|
| 873 |
fileInput.addEventListener('change', () => {
|
| 874 |
+
console.log('File input changed, files:', fileInput.files.length); // Debug
|
| 875 |
+
if (fileInput.files && fileInput.files.length > 0) {
|
| 876 |
+
uploadFiles(fileInput.files);
|
| 877 |
+
}
|
| 878 |
fileInput.value = '';
|
| 879 |
});
|
| 880 |
|
|
|
|
| 891 |
createFolderModal.classList.add('active');
|
| 892 |
setTimeout(() => folderNameInput.focus(), 100);
|
| 893 |
});
|
| 894 |
+
uploadFileBtn.addEventListener('click', (e) => {
|
| 895 |
+
e.preventDefault();
|
| 896 |
+
e.stopPropagation();
|
| 897 |
+
console.log('Upload button clicked'); // Debug
|
| 898 |
newDropdown.classList.remove('active');
|
| 899 |
+
// Use try-catch in case fileInput doesn't exist
|
| 900 |
+
try {
|
| 901 |
+
if (fileInput) {
|
| 902 |
+
fileInput.click();
|
| 903 |
+
console.log('fileInput.click() called'); // Debug
|
| 904 |
+
} else {
|
| 905 |
+
console.error('fileInput element not found');
|
| 906 |
+
showToast('Error: File input not available', 'error');
|
| 907 |
+
}
|
| 908 |
+
} catch (err) {
|
| 909 |
+
console.error('Error clicking file input:', err);
|
| 910 |
+
showToast('Error: Could not open file dialog', 'error');
|
| 911 |
+
}
|
| 912 |
});
|
| 913 |
|
| 914 |
// ─── CREATE FOLDER ────────────────────────────────────────
|
|
|
|
| 950 |
}
|
| 951 |
|
| 952 |
navMyFiles.addEventListener('click', (e) => {
|
| 953 |
+
console.log('navMyFiles clicked'); // Debug
|
| 954 |
e.preventDefault();
|
| 955 |
e.stopPropagation();
|
| 956 |
e.stopImmediatePropagation();
|
| 957 |
+
currentBrowse = 'files';
|
| 958 |
+
currentPath = [];
|
| 959 |
setNavActive(navMyFiles);
|
| 960 |
fetchAndRender();
|
| 961 |
+
return false;
|
| 962 |
});
|
| 963 |
|
| 964 |
navRecent.addEventListener('click', (e) => {
|
| 965 |
+
console.log('navRecent clicked'); // Debug
|
| 966 |
e.preventDefault();
|
| 967 |
+
e.stopPropagation();
|
| 968 |
+
e.stopImmediatePropagation();
|
| 969 |
currentBrowse = 'recent';
|
| 970 |
setNavActive(navRecent);
|
| 971 |
renderRecentView();
|
| 972 |
+
return false;
|
| 973 |
});
|
| 974 |
|
| 975 |
navStarred.addEventListener('click', (e) => {
|
| 976 |
+
console.log('navStarred clicked'); // Debug
|
| 977 |
e.preventDefault();
|
| 978 |
+
e.stopPropagation();
|
| 979 |
+
e.stopImmediatePropagation();
|
| 980 |
currentBrowse = 'starred';
|
| 981 |
setNavActive(navStarred);
|
| 982 |
renderStarredView();
|
| 983 |
+
return false;
|
| 984 |
});
|
| 985 |
|
| 986 |
function renderRecentView() {
|
|
|
|
| 1120 |
// ─── INIT ─────────────────────────────────────────────────
|
| 1121 |
// Initialize offline-first DocVault
|
| 1122 |
(function initApp() {
|
| 1123 |
+
console.log('🚀 DocVault initializing...'); // Debug
|
| 1124 |
+
console.log('API_BASE:', API_BASE); // Debug
|
| 1125 |
+
console.log('USER_ID:', USER_ID); // Debug
|
| 1126 |
+
console.log('DOM Elements check:', {
|
| 1127 |
+
fileInput: !!fileInput,
|
| 1128 |
+
uploadFileBtn: !!uploadFileBtn,
|
| 1129 |
+
foldersContainer: !!foldersContainer,
|
| 1130 |
+
filesContainer: !!filesContainer
|
| 1131 |
+
}); // Debug
|
| 1132 |
+
|
| 1133 |
// Show welcome message
|
| 1134 |
showToast('🎉 Welcome to DocVault! Loading your files...', 'info');
|
| 1135 |
|
index.html
CHANGED
|
@@ -38,29 +38,29 @@
|
|
| 38 |
</div>
|
| 39 |
|
| 40 |
<div class="new-btn-wrapper">
|
| 41 |
-
<button class="btn-primary new-btn" id="newBtn">
|
| 42 |
<i class="ph-bold ph-plus"></i> New
|
| 43 |
</button>
|
| 44 |
<div class="new-dropdown" id="newDropdown">
|
| 45 |
-
<button class="new-dropdown-item" id="createFolderBtn">
|
| 46 |
<i class="ph-fill ph-folder-plus"></i> Create Folder
|
| 47 |
</button>
|
| 48 |
-
<button class="new-dropdown-item" id="uploadFileBtn">
|
| 49 |
<i class="ph-fill ph-upload-simple"></i> Upload File
|
| 50 |
</button>
|
| 51 |
</div>
|
| 52 |
</div>
|
| 53 |
|
| 54 |
<nav class="sidebar-nav">
|
| 55 |
-
<
|
| 56 |
<i class="ph-fill ph-folder"></i> My Files
|
| 57 |
-
</
|
| 58 |
-
<
|
| 59 |
<i class="ph-fill ph-clock-counter-clockwise"></i> Recent
|
| 60 |
-
</
|
| 61 |
-
<
|
| 62 |
<i class="ph-fill ph-star"></i> Starred
|
| 63 |
-
</
|
| 64 |
</nav>
|
| 65 |
|
| 66 |
<div class="sidebar-bottom">
|
|
@@ -102,8 +102,8 @@
|
|
| 102 |
<div class="section-header">
|
| 103 |
<h2>Folders</h2>
|
| 104 |
<div class="view-toggles">
|
| 105 |
-
<button class="icon-btn active" id="viewGrid" title="Grid"><i class="ph-fill ph-squares-four"></i></button>
|
| 106 |
-
<button class="icon-btn" id="viewList" title="List"><i class="ph-fill ph-list-dashes"></i></button>
|
| 107 |
</div>
|
| 108 |
</div>
|
| 109 |
<div class="grid-container" id="foldersContainer"></div>
|
|
@@ -121,14 +121,14 @@
|
|
| 121 |
<!-- Create Folder -->
|
| 122 |
<div class="modal-overlay" id="createFolderModal">
|
| 123 |
<div class="modal glass-panel">
|
| 124 |
-
<button class="close-modal" id="closeNameModal"><i class="ph-bold ph-x"></i></button>
|
| 125 |
<h3><i class="ph-fill ph-folder-plus" style="color:var(--folder-color);margin-right:10px"></i>New Folder</h3>
|
| 126 |
<div class="input-group">
|
| 127 |
<input type="text" id="folderNameInput" placeholder="Enter folder name..." autocomplete="off">
|
| 128 |
</div>
|
| 129 |
<div class="modal-footer">
|
| 130 |
-
<button class="btn-secondary" id="cancelFolderBtn">Cancel</button>
|
| 131 |
-
<button class="btn-primary" id="confirmFolderBtn">Create</button>
|
| 132 |
</div>
|
| 133 |
</div>
|
| 134 |
</div>
|
|
@@ -136,14 +136,14 @@
|
|
| 136 |
<!-- Delete Confirmation -->
|
| 137 |
<div class="modal-overlay" id="deleteModal">
|
| 138 |
<div class="modal glass-panel" style="max-width:360px">
|
| 139 |
-
<button class="close-modal" id="closeDeleteModal"><i class="ph-bold ph-x"></i></button>
|
| 140 |
<div class="delete-icon-wrap"><i class="ph-fill ph-warning"></i></div>
|
| 141 |
<p style="text-align:center; margin-bottom:20px; font-size:15px; color:var(--text-main);">
|
| 142 |
Are you sure you want to delete <strong>this item</strong>?
|
| 143 |
</p>
|
| 144 |
<div class="modal-footer" style="justify-content:center;gap:16px">
|
| 145 |
-
<button class="btn-secondary" id="cancelDeleteBtn">Cancel</button>
|
| 146 |
-
<button class="btn-danger" id="confirmDeleteBtn"><i class="ph-fill ph-trash"></i> Delete</button>
|
| 147 |
</div>
|
| 148 |
</div>
|
| 149 |
</div>
|
|
@@ -151,15 +151,15 @@
|
|
| 151 |
<!-- Rename Modal -->
|
| 152 |
<div class="modal-overlay" id="renameModal">
|
| 153 |
<div class="modal glass-panel" style="max-width:400px">
|
| 154 |
-
<button class="close-modal" id="closeRenameModal"><i class="ph-bold ph-x"></i></button>
|
| 155 |
<h3><i class="ph-fill ph-pencil-simple" style="color:var(--primary-color)"></i> Rename File</h3>
|
| 156 |
<div class="input-group">
|
| 157 |
<label class="input-label">New Name</label>
|
| 158 |
<input type="text" id="renameInput" placeholder="Enter new name..." autocomplete="off">
|
| 159 |
</div>
|
| 160 |
<div class="modal-footer">
|
| 161 |
-
<button class="btn-secondary" id="cancelRenameBtn">Cancel</button>
|
| 162 |
-
<button class="btn-primary" id="confirmRenameBtn"><i class="ph-fill ph-check"></i> Rename</button>
|
| 163 |
</div>
|
| 164 |
</div>
|
| 165 |
</div>
|
|
@@ -175,8 +175,8 @@
|
|
| 175 |
<span id="previewFileName">filename.pdf</span>
|
| 176 |
</div>
|
| 177 |
<div style="display:flex; gap:12px">
|
| 178 |
-
<button class="icon-btn" id="downloadFromPreview" title="Download"><i class="ph-bold ph-download-simple"></i></button>
|
| 179 |
-
<button class="close-modal" id="closePreviewModal" style="position:static"><i class="ph-bold ph-x"></i></button>
|
| 180 |
</div>
|
| 181 |
</div>
|
| 182 |
<div class="preview-body" id="previewBody">
|
|
@@ -185,6 +185,6 @@
|
|
| 185 |
</div>
|
| 186 |
</div>
|
| 187 |
|
| 188 |
-
<script src="
|
| 189 |
</body>
|
| 190 |
</html>
|
|
|
|
| 38 |
</div>
|
| 39 |
|
| 40 |
<div class="new-btn-wrapper">
|
| 41 |
+
<button type="button" class="btn-primary new-btn" id="newBtn">
|
| 42 |
<i class="ph-bold ph-plus"></i> New
|
| 43 |
</button>
|
| 44 |
<div class="new-dropdown" id="newDropdown">
|
| 45 |
+
<button type="button" class="new-dropdown-item" id="createFolderBtn">
|
| 46 |
<i class="ph-fill ph-folder-plus"></i> Create Folder
|
| 47 |
</button>
|
| 48 |
+
<button type="button" class="new-dropdown-item" id="uploadFileBtn">
|
| 49 |
<i class="ph-fill ph-upload-simple"></i> Upload File
|
| 50 |
</button>
|
| 51 |
</div>
|
| 52 |
</div>
|
| 53 |
|
| 54 |
<nav class="sidebar-nav">
|
| 55 |
+
<button type="button" class="nav-item active" id="navMyFiles">
|
| 56 |
<i class="ph-fill ph-folder"></i> My Files
|
| 57 |
+
</button>
|
| 58 |
+
<button type="button" class="nav-item" id="navRecent">
|
| 59 |
<i class="ph-fill ph-clock-counter-clockwise"></i> Recent
|
| 60 |
+
</button>
|
| 61 |
+
<button type="button" class="nav-item" id="navStarred">
|
| 62 |
<i class="ph-fill ph-star"></i> Starred
|
| 63 |
+
</button>
|
| 64 |
</nav>
|
| 65 |
|
| 66 |
<div class="sidebar-bottom">
|
|
|
|
| 102 |
<div class="section-header">
|
| 103 |
<h2>Folders</h2>
|
| 104 |
<div class="view-toggles">
|
| 105 |
+
<button type="button" class="icon-btn active" id="viewGrid" title="Grid"><i class="ph-fill ph-squares-four"></i></button>
|
| 106 |
+
<button type="button" class="icon-btn" id="viewList" title="List"><i class="ph-fill ph-list-dashes"></i></button>
|
| 107 |
</div>
|
| 108 |
</div>
|
| 109 |
<div class="grid-container" id="foldersContainer"></div>
|
|
|
|
| 121 |
<!-- Create Folder -->
|
| 122 |
<div class="modal-overlay" id="createFolderModal">
|
| 123 |
<div class="modal glass-panel">
|
| 124 |
+
<button type="button" class="close-modal" id="closeNameModal"><i class="ph-bold ph-x"></i></button>
|
| 125 |
<h3><i class="ph-fill ph-folder-plus" style="color:var(--folder-color);margin-right:10px"></i>New Folder</h3>
|
| 126 |
<div class="input-group">
|
| 127 |
<input type="text" id="folderNameInput" placeholder="Enter folder name..." autocomplete="off">
|
| 128 |
</div>
|
| 129 |
<div class="modal-footer">
|
| 130 |
+
<button type="button" class="btn-secondary" id="cancelFolderBtn">Cancel</button>
|
| 131 |
+
<button type="button" class="btn-primary" id="confirmFolderBtn">Create</button>
|
| 132 |
</div>
|
| 133 |
</div>
|
| 134 |
</div>
|
|
|
|
| 136 |
<!-- Delete Confirmation -->
|
| 137 |
<div class="modal-overlay" id="deleteModal">
|
| 138 |
<div class="modal glass-panel" style="max-width:360px">
|
| 139 |
+
<button type="button" class="close-modal" id="closeDeleteModal"><i class="ph-bold ph-x"></i></button>
|
| 140 |
<div class="delete-icon-wrap"><i class="ph-fill ph-warning"></i></div>
|
| 141 |
<p style="text-align:center; margin-bottom:20px; font-size:15px; color:var(--text-main);">
|
| 142 |
Are you sure you want to delete <strong>this item</strong>?
|
| 143 |
</p>
|
| 144 |
<div class="modal-footer" style="justify-content:center;gap:16px">
|
| 145 |
+
<button type="button" class="btn-secondary" id="cancelDeleteBtn">Cancel</button>
|
| 146 |
+
<button type="button" class="btn-danger" id="confirmDeleteBtn"><i class="ph-fill ph-trash"></i> Delete</button>
|
| 147 |
</div>
|
| 148 |
</div>
|
| 149 |
</div>
|
|
|
|
| 151 |
<!-- Rename Modal -->
|
| 152 |
<div class="modal-overlay" id="renameModal">
|
| 153 |
<div class="modal glass-panel" style="max-width:400px">
|
| 154 |
+
<button type="button" class="close-modal" id="closeRenameModal"><i class="ph-bold ph-x"></i></button>
|
| 155 |
<h3><i class="ph-fill ph-pencil-simple" style="color:var(--primary-color)"></i> Rename File</h3>
|
| 156 |
<div class="input-group">
|
| 157 |
<label class="input-label">New Name</label>
|
| 158 |
<input type="text" id="renameInput" placeholder="Enter new name..." autocomplete="off">
|
| 159 |
</div>
|
| 160 |
<div class="modal-footer">
|
| 161 |
+
<button type="button" class="btn-secondary" id="cancelRenameBtn">Cancel</button>
|
| 162 |
+
<button type="button" class="btn-primary" id="confirmRenameBtn"><i class="ph-fill ph-check"></i> Rename</button>
|
| 163 |
</div>
|
| 164 |
</div>
|
| 165 |
</div>
|
|
|
|
| 175 |
<span id="previewFileName">filename.pdf</span>
|
| 176 |
</div>
|
| 177 |
<div style="display:flex; gap:12px">
|
| 178 |
+
<button type="button" class="icon-btn" id="downloadFromPreview" title="Download"><i class="ph-bold ph-download-simple"></i></button>
|
| 179 |
+
<button type="button" class="close-modal" id="closePreviewModal" style="position:static"><i class="ph-bold ph-x"></i></button>
|
| 180 |
</div>
|
| 181 |
</div>
|
| 182 |
<div class="preview-body" id="previewBody">
|
|
|
|
| 185 |
</div>
|
| 186 |
</div>
|
| 187 |
|
| 188 |
+
<script type="module" src="js/main.js?v=2"></script>
|
| 189 |
</body>
|
| 190 |
</html>
|
js/api/hfService.js
CHANGED
|
@@ -37,34 +37,42 @@ class HFService {
|
|
| 37 |
}
|
| 38 |
}
|
| 39 |
|
| 40 |
-
async listFiles(path = ''
|
| 41 |
-
const cacheKey = `list-${path}
|
| 42 |
const cached = this.cache.get(cacheKey);
|
| 43 |
if (cached && (Date.now() - cached.timestamp < CACHE_TTL)) {
|
| 44 |
return cached.data;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
const
|
| 48 |
-
const
|
|
|
|
|
|
|
|
|
|
| 49 |
const data = await res.json();
|
| 50 |
|
| 51 |
const result = { files: [], folders: [] };
|
| 52 |
|
| 53 |
-
if (
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
result.folders.push({
|
| 65 |
path: item.path,
|
| 66 |
-
name: item.
|
| 67 |
-
type: '
|
| 68 |
});
|
| 69 |
}
|
| 70 |
}
|
|
@@ -75,17 +83,20 @@ class HFService {
|
|
| 75 |
}
|
| 76 |
|
| 77 |
async uploadFile(file, destPath) {
|
| 78 |
-
const
|
| 79 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
const res = await this.fetchWithRetry(url, {
|
| 82 |
method: 'POST',
|
| 83 |
-
headers: { '
|
| 84 |
-
body:
|
| 85 |
-
path: destPath,
|
| 86 |
-
content: base64Content,
|
| 87 |
-
summary: `Upload ${destPath.split('/').pop()}`
|
| 88 |
-
}),
|
| 89 |
});
|
| 90 |
|
| 91 |
this.clearCache();
|
|
@@ -93,11 +104,11 @@ class HFService {
|
|
| 93 |
}
|
| 94 |
|
| 95 |
async deleteFile(path) {
|
| 96 |
-
const url = `${this.apiBase}/delete`;
|
| 97 |
await this.fetchWithRetry(url, {
|
| 98 |
method: 'POST',
|
| 99 |
-
headers: { 'Content-Type': 'application/json' },
|
| 100 |
-
body: JSON.stringify({ path }),
|
| 101 |
});
|
| 102 |
|
| 103 |
this.clearCache();
|
|
@@ -108,24 +119,14 @@ class HFService {
|
|
| 108 |
const url = `${this.apiBase}/delete-folder`;
|
| 109 |
const res = await this.fetchWithRetry(url, {
|
| 110 |
method: 'POST',
|
| 111 |
-
headers: { 'Content-Type': 'application/json' },
|
| 112 |
-
body: JSON.stringify({
|
| 113 |
});
|
| 114 |
|
| 115 |
this.clearCache();
|
| 116 |
return await res.json();
|
| 117 |
}
|
| 118 |
|
| 119 |
-
async fileToBase64(file) {
|
| 120 |
-
return new Promise((resolve, reject) => {
|
| 121 |
-
const reader = new FileReader();
|
| 122 |
-
const blob = file instanceof File ? file : file.content;
|
| 123 |
-
reader.readAsDataURL(blob);
|
| 124 |
-
reader.onload = () => resolve(reader.result.split(',')[1]);
|
| 125 |
-
reader.onerror = reject;
|
| 126 |
-
});
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
clearCache() {
|
| 130 |
this.cache.clear();
|
| 131 |
}
|
|
|
|
| 37 |
}
|
| 38 |
}
|
| 39 |
|
| 40 |
+
async listFiles(path = '') {
|
| 41 |
+
const cacheKey = `list-${path}`;
|
| 42 |
const cached = this.cache.get(cacheKey);
|
| 43 |
if (cached && (Date.now() - cached.timestamp < CACHE_TTL)) {
|
| 44 |
return cached.data;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
const queryPath = path ? `?folder_path=${encodeURIComponent(path)}` : '';
|
| 48 |
+
const url = `${this.apiBase}/list${queryPath}`;
|
| 49 |
+
|
| 50 |
+
// Add X-User-ID header to match app.py expected requests
|
| 51 |
+
const res = await this.fetchWithRetry(url, { headers: { 'X-User-ID': 'default_user' } });
|
| 52 |
const data = await res.json();
|
| 53 |
|
| 54 |
const result = { files: [], folders: [] };
|
| 55 |
|
| 56 |
+
if (data && data.success) {
|
| 57 |
+
if (data.files) {
|
| 58 |
+
for (const item of data.files) {
|
| 59 |
+
if (!item.path.endsWith('/.gitkeep') && item.path !== '.gitkeep') {
|
| 60 |
+
result.files.push({
|
| 61 |
+
path: item.path,
|
| 62 |
+
name: item.name,
|
| 63 |
+
size: item.size || 0,
|
| 64 |
+
type: 'file',
|
| 65 |
+
lastModified: item.modified_at
|
| 66 |
+
});
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
if (data.folders) {
|
| 71 |
+
for (const item of data.folders) {
|
| 72 |
result.folders.push({
|
| 73 |
path: item.path,
|
| 74 |
+
name: item.name,
|
| 75 |
+
type: 'folder'
|
| 76 |
});
|
| 77 |
}
|
| 78 |
}
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
async uploadFile(file, destPath) {
|
| 86 |
+
const formData = new FormData();
|
| 87 |
+
const folderPath = destPath.includes('/') ? destPath.substring(0, destPath.lastIndexOf('/')) : '';
|
| 88 |
+
const filename = file instanceof File ? file.name : destPath.split('/').pop();
|
| 89 |
+
const fileBlob = file instanceof File ? file : new Blob([file.content || '']);
|
| 90 |
+
|
| 91 |
+
formData.append('folder_path', folderPath);
|
| 92 |
+
formData.append('file', fileBlob, filename);
|
| 93 |
+
|
| 94 |
+
const url = `${this.apiBase}/upload-file`;
|
| 95 |
|
| 96 |
const res = await this.fetchWithRetry(url, {
|
| 97 |
method: 'POST',
|
| 98 |
+
headers: { 'X-User-ID': 'default_user' }, // Let browser set Content-Type with boundary
|
| 99 |
+
body: formData
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
});
|
| 101 |
|
| 102 |
this.clearCache();
|
|
|
|
| 104 |
}
|
| 105 |
|
| 106 |
async deleteFile(path) {
|
| 107 |
+
const url = `${this.apiBase}/delete-file`;
|
| 108 |
await this.fetchWithRetry(url, {
|
| 109 |
method: 'POST',
|
| 110 |
+
headers: { 'Content-Type': 'application/json', 'X-User-ID': 'default_user' },
|
| 111 |
+
body: JSON.stringify({ file_path: path }),
|
| 112 |
});
|
| 113 |
|
| 114 |
this.clearCache();
|
|
|
|
| 119 |
const url = `${this.apiBase}/delete-folder`;
|
| 120 |
const res = await this.fetchWithRetry(url, {
|
| 121 |
method: 'POST',
|
| 122 |
+
headers: { 'Content-Type': 'application/json', 'X-User-ID': 'default_user' },
|
| 123 |
+
body: JSON.stringify({ folder_path: folderPath, force: true }),
|
| 124 |
});
|
| 125 |
|
| 126 |
this.clearCache();
|
| 127 |
return await res.json();
|
| 128 |
}
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
clearCache() {
|
| 131 |
this.cache.clear();
|
| 132 |
}
|
js/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { hfService } from './api/hfService.js';
|
| 2 |
import { stateManager } from './state/stateManager.js';
|
| 3 |
import { UIRenderer } from './ui/uiRenderer.js';
|
| 4 |
import { getFileUrl, isImage, isPDF, isText } from './utils/formatters.js';
|
|
@@ -97,11 +97,15 @@ class App {
|
|
| 97 |
e.stopPropagation();
|
| 98 |
document.getElementById('newDropdown').classList.toggle('active');
|
| 99 |
};
|
| 100 |
-
document.getElementById('uploadFileBtn').onclick = () => {
|
|
|
|
|
|
|
| 101 |
document.getElementById('newDropdown').classList.remove('active');
|
| 102 |
document.getElementById('fileInput').click();
|
| 103 |
};
|
| 104 |
-
document.getElementById('createFolderBtn').onclick = () => {
|
|
|
|
|
|
|
| 105 |
document.getElementById('newDropdown').classList.remove('active');
|
| 106 |
document.getElementById('createFolderModal').classList.add('active');
|
| 107 |
document.getElementById('folderNameInput').value = '';
|
|
|
|
| 1 |
+
import { hfService } from './api/hfService.js?v=2';
|
| 2 |
import { stateManager } from './state/stateManager.js';
|
| 3 |
import { UIRenderer } from './ui/uiRenderer.js';
|
| 4 |
import { getFileUrl, isImage, isPDF, isText } from './utils/formatters.js';
|
|
|
|
| 97 |
e.stopPropagation();
|
| 98 |
document.getElementById('newDropdown').classList.toggle('active');
|
| 99 |
};
|
| 100 |
+
document.getElementById('uploadFileBtn').onclick = (e) => {
|
| 101 |
+
e.preventDefault();
|
| 102 |
+
e.stopPropagation();
|
| 103 |
document.getElementById('newDropdown').classList.remove('active');
|
| 104 |
document.getElementById('fileInput').click();
|
| 105 |
};
|
| 106 |
+
document.getElementById('createFolderBtn').onclick = (e) => {
|
| 107 |
+
e.preventDefault();
|
| 108 |
+
e.stopPropagation();
|
| 109 |
document.getElementById('newDropdown').classList.remove('active');
|
| 110 |
document.getElementById('createFolderModal').classList.add('active');
|
| 111 |
document.getElementById('folderNameInput').value = '';
|
js/ui/uiRenderer.js
CHANGED
|
@@ -91,6 +91,8 @@ export class UIRenderer {
|
|
| 91 |
<div class="item-meta">Folder</div>`;
|
| 92 |
|
| 93 |
card.onclick = (e) => {
|
|
|
|
|
|
|
| 94 |
if (e.target.closest('.card-menu')) return;
|
| 95 |
onFolderClick(folder.name);
|
| 96 |
};
|
|
|
|
| 91 |
<div class="item-meta">Folder</div>`;
|
| 92 |
|
| 93 |
card.onclick = (e) => {
|
| 94 |
+
e.preventDefault();
|
| 95 |
+
e.stopPropagation();
|
| 96 |
if (e.target.closest('.card-menu')) return;
|
| 97 |
onFolderClick(folder.name);
|
| 98 |
};
|
server/app.py
CHANGED
|
@@ -39,6 +39,7 @@ def create_app():
|
|
| 39 |
def add_cache_headers(response):
|
| 40 |
if response.content_type and ('text/html' in response.content_type or
|
| 41 |
'text/javascript' in response.content_type or
|
|
|
|
| 42 |
'text/css' in response.content_type):
|
| 43 |
response.cache_control.max_age = 0
|
| 44 |
response.cache_control.no_cache = True
|
|
|
|
| 39 |
def add_cache_headers(response):
|
| 40 |
if response.content_type and ('text/html' in response.content_type or
|
| 41 |
'text/javascript' in response.content_type or
|
| 42 |
+
'application/javascript' in response.content_type or
|
| 43 |
'text/css' in response.content_type):
|
| 44 |
response.cache_control.max_age = 0
|
| 45 |
response.cache_control.no_cache = True
|