Update.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { api } from '../api.js';
|
||||
|
||||
export class DeletedFiles extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.deletedFiles = [];
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await this.loadDeletedFiles();
|
||||
}
|
||||
|
||||
async loadDeletedFiles() {
|
||||
try {
|
||||
this.deletedFiles = await api.listDeletedFiles();
|
||||
this.render();
|
||||
} catch (error) {
|
||||
console.error('Failed to load deleted files:', error);
|
||||
this.innerHTML = '<p class="error-message">Failed to load deleted files.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.deletedFiles.length === 0) {
|
||||
this.innerHTML = `
|
||||
<div class="deleted-files-container">
|
||||
<h2>Deleted Files</h2>
|
||||
<p class="empty-state">No deleted files found.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
this.innerHTML = `
|
||||
<div class="deleted-files-container">
|
||||
<h2>Deleted Files</h2>
|
||||
<div class="file-grid">
|
||||
${this.deletedFiles.map(file => this.renderDeletedFile(file)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.attachListeners();
|
||||
}
|
||||
|
||||
renderDeletedFile(file) {
|
||||
const icon = this.getFileIcon(file.mime_type);
|
||||
const size = this.formatFileSize(file.size);
|
||||
const deletedDate = new Date(file.deleted_at).toLocaleDateString();
|
||||
|
||||
return `
|
||||
<div class="file-item deleted-item" data-file-id="${file.id}">
|
||||
<div class="file-icon">${icon}</div>
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-size">${size}</div>
|
||||
<div class="file-deleted-date">Deleted: ${deletedDate}</div>
|
||||
<div class="file-actions-menu">
|
||||
<button class="button button-danger action-btn" data-action="restore" data-id="${file.id}">Restore</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getFileIcon(mimeType) {
|
||||
if (mimeType.startsWith('image/')) return '📷';
|
||||
if (mimeType.startsWith('video/')) return '🎥';
|
||||
if (mimeType.startsWith('audio/')) return '🎵';
|
||||
if (mimeType.includes('pdf')) return '📄';
|
||||
if (mimeType.includes('text')) return '📄';
|
||||
return '📄';
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
return (bytes / 1073741824).toFixed(1) + ' GB';
|
||||
}
|
||||
|
||||
attachListeners() {
|
||||
this.querySelectorAll('.action-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const action = btn.dataset.action;
|
||||
const id = parseInt(btn.dataset.id);
|
||||
if (action === 'restore') {
|
||||
await this.handleRestore(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async handleRestore(fileId) {
|
||||
if (confirm('Are you sure you want to restore this file?')) {
|
||||
try {
|
||||
await api.restoreFile(fileId);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'File restored successfully!', type: 'success' }
|
||||
}));
|
||||
await this.loadDeletedFiles(); // Reload the list
|
||||
} catch (error) {
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to restore file: ' + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('deleted-files', DeletedFiles);
|
||||
@@ -6,6 +6,8 @@ export class FileList extends HTMLElement {
|
||||
this.currentFolderId = null;
|
||||
this.files = [];
|
||||
this.folders = [];
|
||||
this.selectedFiles = new Set();
|
||||
this.selectedFolders = new Set();
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
@@ -17,19 +19,31 @@ export class FileList extends HTMLElement {
|
||||
try {
|
||||
this.folders = await api.listFolders(folderId);
|
||||
this.files = await api.listFiles(folderId);
|
||||
this.selectedFiles.clear();
|
||||
this.selectedFolders.clear();
|
||||
this.render();
|
||||
} catch (error) {
|
||||
console.error('Failed to load contents:', error);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to load contents: ' + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
setFiles(files) {
|
||||
this.files = files;
|
||||
this.folders = [];
|
||||
this.selectedFiles.clear();
|
||||
this.selectedFolders.clear();
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasSelected = this.selectedFiles.size > 0 || this.selectedFolders.size > 0;
|
||||
const allFilesSelected = this.files.length > 0 && this.selectedFiles.size === this.files.length;
|
||||
const allFoldersSelected = this.folders.length > 0 && this.selectedFolders.size === this.folders.length;
|
||||
const allSelected = (this.files.length + this.folders.length) > 0 && allFilesSelected && allFoldersSelected;
|
||||
|
||||
this.innerHTML = `
|
||||
<div class="file-list-container">
|
||||
<div class="file-list-header">
|
||||
@@ -40,6 +54,16 @@ export class FileList extends HTMLElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="batch-actions" style="display: ${hasSelected ? 'flex' : 'none'};">
|
||||
<input type="checkbox" id="select-all" ${allSelected ? 'checked' : ''}>
|
||||
<label for="select-all">Select All</label>
|
||||
<button class="button button-small button-danger" id="batch-delete-btn">Delete Selected</button>
|
||||
<button class="button button-small" id="batch-move-btn">Move Selected</button>
|
||||
<button class="button button-small" id="batch-copy-btn">Copy Selected</button>
|
||||
<button class="button button-small" id="batch-star-btn">Star Selected</button>
|
||||
<button class="button button-small" id="batch-unstar-btn">Unstar Selected</button>
|
||||
</div>
|
||||
|
||||
<div class="file-grid">
|
||||
${this.folders.map(folder => this.renderFolder(folder)).join('')}
|
||||
${this.files.map(file => this.renderFile(file)).join('')}
|
||||
@@ -51,23 +75,32 @@ export class FileList extends HTMLElement {
|
||||
}
|
||||
|
||||
renderFolder(folder) {
|
||||
const isSelected = this.selectedFolders.has(folder.id);
|
||||
const starIcon = folder.is_starred ? '★' : '☆'; // Filled star or empty star
|
||||
const starAction = folder.is_starred ? 'unstar-folder' : 'star-folder';
|
||||
return `
|
||||
<div class="file-item folder-item" data-folder-id="${folder.id}">
|
||||
<input type="checkbox" class="select-item" data-type="folder" data-id="${folder.id}" ${isSelected ? 'checked' : ''}>
|
||||
<div class="file-icon">📁</div>
|
||||
<div class="file-name">${folder.name}</div>
|
||||
<div class="file-actions-menu">
|
||||
<button class="action-btn" data-action="delete-folder" data-id="${folder.id}">Delete</button>
|
||||
<button class="action-btn star-btn" data-action="${starAction}" data-id="${folder.id}">${starIcon}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderFile(file) {
|
||||
const isSelected = this.selectedFiles.has(file.id);
|
||||
const icon = this.getFileIcon(file.mime_type);
|
||||
const size = this.formatFileSize(file.size);
|
||||
const starIcon = file.is_starred ? '★' : '☆'; // Filled star or empty star
|
||||
const starAction = file.is_starred ? 'unstar-file' : 'star-file';
|
||||
|
||||
return `
|
||||
<div class="file-item" data-file-id="${file.id}">
|
||||
<input type="checkbox" class="select-item" data-type="file" data-id="${file.id}" ${isSelected ? 'checked' : ''}>
|
||||
<div class="file-icon">${icon}</div>
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-size">${size}</div>
|
||||
@@ -76,6 +109,7 @@ export class FileList extends HTMLElement {
|
||||
<button class="action-btn" data-action="rename" data-id="${file.id}">Rename</button>
|
||||
<button class="action-btn" data-action="delete" data-id="${file.id}">Delete</button>
|
||||
<button class="action-btn" data-action="share" data-id="${file.id}">Share</button>
|
||||
<button class="action-btn star-btn" data-action="${starAction}" data-id="${file.id}">${starIcon}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -115,7 +149,7 @@ export class FileList extends HTMLElement {
|
||||
|
||||
this.querySelectorAll('.file-item:not(.folder-item)').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
if (!e.target.classList.contains('action-btn')) {
|
||||
if (!e.target.classList.contains('action-btn') && !e.target.classList.contains('select-item')) {
|
||||
const fileId = parseInt(item.dataset.fileId);
|
||||
const file = this.files.find(f => f.id === fileId);
|
||||
this.dispatchEvent(new CustomEvent('photo-click', {
|
||||
@@ -134,6 +168,112 @@ export class FileList extends HTMLElement {
|
||||
await this.handleAction(action, id);
|
||||
});
|
||||
});
|
||||
|
||||
this.querySelectorAll('.select-item').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const type = e.target.dataset.type;
|
||||
const id = parseInt(e.target.dataset.id);
|
||||
this.toggleSelectItem(type, id, e.target.checked);
|
||||
});
|
||||
});
|
||||
|
||||
this.querySelector('#select-all')?.addEventListener('change', (e) => {
|
||||
this.toggleSelectAll(e.target.checked);
|
||||
});
|
||||
|
||||
this.querySelector('#batch-delete-btn')?.addEventListener('click', () => this.handleBatchAction('delete'));
|
||||
this.querySelector('#batch-move-btn')?.addEventListener('click', () => this.handleBatchAction('move'));
|
||||
this.querySelector('#batch-copy-btn')?.addEventListener('click', () => this.handleBatchAction('copy'));
|
||||
this.querySelector('#batch-star-btn')?.addEventListener('click', () => this.handleBatchAction('star'));
|
||||
this.querySelector('#batch-unstar-btn')?.addEventListener('click', () => this.handleBatchAction('unstar'));
|
||||
|
||||
this.updateBatchActionVisibility();
|
||||
}
|
||||
|
||||
toggleSelectItem(type, id, checked) {
|
||||
if (type === 'file') {
|
||||
if (checked) {
|
||||
this.selectedFiles.add(id);
|
||||
} else {
|
||||
this.selectedFiles.delete(id);
|
||||
}
|
||||
} else if (type === 'folder') {
|
||||
if (checked) {
|
||||
this.selectedFolders.add(id);
|
||||
} else {
|
||||
this.selectedFolders.delete(id);
|
||||
}
|
||||
}
|
||||
this.updateBatchActionVisibility();
|
||||
}
|
||||
|
||||
toggleSelectAll(checked) {
|
||||
this.selectedFiles.clear();
|
||||
this.selectedFolders.clear();
|
||||
|
||||
if (checked) {
|
||||
this.files.forEach(file => this.selectedFiles.add(file.id));
|
||||
this.folders.forEach(folder => this.selectedFolders.add(folder.id));
|
||||
}
|
||||
this.render(); // Re-render to update checkboxes
|
||||
}
|
||||
|
||||
updateBatchActionVisibility() {
|
||||
const batchActionsDiv = this.querySelector('.batch-actions');
|
||||
if (batchActionsDiv) {
|
||||
if (this.selectedFiles.size > 0 || this.selectedFolders.size > 0) {
|
||||
batchActionsDiv.style.display = 'flex';
|
||||
} else {
|
||||
batchActionsDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleBatchAction(action) {
|
||||
if ((this.selectedFiles.size === 0 && this.selectedFolders.size === 0)) {
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'No items selected for batch operation.', type: 'info' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to ${action} ${this.selectedFiles.size + this.selectedFolders.size} items?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let targetFolderId = null;
|
||||
if (action === 'move' || action === 'copy') {
|
||||
const folderName = prompt('Enter target folder ID (leave empty for root):');
|
||||
if (folderName !== null) {
|
||||
targetFolderId = folderName === '' ? null : parseInt(folderName);
|
||||
if (folderName !== '' && isNaN(targetFolderId)) {
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Invalid folder ID.', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return; // User cancelled
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selectedFiles.size > 0) {
|
||||
await api.batchFileOperations(action, Array.from(this.selectedFiles), targetFolderId);
|
||||
}
|
||||
if (this.selectedFolders.size > 0) {
|
||||
await api.batchFolderOperations(action, Array.from(this.selectedFolders), targetFolderId);
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: `Batch ${action} successful!`, type: 'success' }
|
||||
}));
|
||||
await this.loadContents(this.currentFolderId); // Reload contents
|
||||
} catch (error) {
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: `Batch ${action} failed: ` + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
triggerCreateFolder() {
|
||||
@@ -147,7 +287,9 @@ export class FileList extends HTMLElement {
|
||||
await api.createFolder(name, this.currentFolderId);
|
||||
await this.loadContents(this.currentFolderId);
|
||||
} catch (error) {
|
||||
alert('Failed to create folder: ' + error.message);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to create folder: ' + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +306,9 @@ export class FileList extends HTMLElement {
|
||||
a.download = file.name;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'File downloaded successfully!', type: 'success' }
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'rename':
|
||||
@@ -171,6 +316,9 @@ export class FileList extends HTMLElement {
|
||||
if (newName) {
|
||||
await api.renameFile(id, newName);
|
||||
await this.loadContents(this.currentFolderId);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'File renamed successfully!', type: 'success' }
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -178,6 +326,9 @@ export class FileList extends HTMLElement {
|
||||
if (confirm('Are you sure you want to delete this file?')) {
|
||||
await api.deleteFile(id);
|
||||
await this.loadContents(this.currentFolderId);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'File deleted successfully!', type: 'success' }
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -185,15 +336,48 @@ export class FileList extends HTMLElement {
|
||||
if (confirm('Are you sure you want to delete this folder?')) {
|
||||
await api.deleteFolder(id);
|
||||
await this.loadContents(this.currentFolderId);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Folder deleted successfully!', type: 'success' }
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'share':
|
||||
this.dispatchEvent(new CustomEvent('share-request', { detail: { fileId: id } }));
|
||||
break;
|
||||
case 'star-file':
|
||||
await api.starFile(id);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'File starred successfully!', type: 'success' }
|
||||
}));
|
||||
await this.loadContents(this.currentFolderId);
|
||||
break;
|
||||
case 'unstar-file':
|
||||
await api.unstarFile(id);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'File unstarred successfully!', type: 'success' }
|
||||
}));
|
||||
await this.loadContents(this.currentFolderId);
|
||||
break;
|
||||
case 'star-folder':
|
||||
await api.starFolder(id);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Folder starred successfully!', type: 'success' }
|
||||
}));
|
||||
await this.loadContents(this.currentFolderId);
|
||||
break;
|
||||
case 'unstar-folder':
|
||||
await api.unstarFolder(id);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Folder unstarred successfully!', type: 'success' }
|
||||
}));
|
||||
await this.loadContents(this.currentFolderId);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Action failed: ' + error.message);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Action failed: ' + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ import './file-upload.js';
|
||||
import './share-modal.js';
|
||||
import './photo-gallery.js';
|
||||
import './file-preview.js';
|
||||
import './deleted-files.js';
|
||||
import './admin-dashboard.js';
|
||||
import './toast-notification.js';
|
||||
import './starred-items.js';
|
||||
import './recent-files.js';
|
||||
import './shared-items.js'; // Import the new component
|
||||
import { shortcuts } from '../shortcuts.js';
|
||||
|
||||
export class RBoxApp extends HTMLElement {
|
||||
@@ -17,6 +23,22 @@ export class RBoxApp extends HTMLElement {
|
||||
|
||||
async connectedCallback() {
|
||||
await this.init();
|
||||
this.addEventListener('show-toast', this.handleShowToast);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('show-toast', this.handleShowToast);
|
||||
}
|
||||
|
||||
handleShowToast = (event) => {
|
||||
const { message, type, duration } = event.detail;
|
||||
this.showToast(message, type, duration);
|
||||
}
|
||||
|
||||
showToast(message, type = 'info', duration = 3000) {
|
||||
const toast = document.createElement('toast-notification');
|
||||
document.body.appendChild(toast);
|
||||
toast.show(message, type, duration);
|
||||
}
|
||||
|
||||
async init() {
|
||||
@@ -63,6 +85,7 @@ export class RBoxApp extends HTMLElement {
|
||||
<li><a href="#" class="nav-link" data-view="photos">Photo Gallery</a></li>
|
||||
<li><a href="#" class="nav-link" data-view="shared">Shared Items</a></li>
|
||||
<li><a href="#" class="nav-link" data-view="deleted">Deleted Files</a></li>
|
||||
${this.user && this.user.is_superuser ? `<li><a href="#" class="nav-link" data-view="admin">Admin Dashboard</a></li>` : ''}
|
||||
</ul>
|
||||
<h3 class="nav-title">Quick Access</h3>
|
||||
<ul class="nav-list">
|
||||
@@ -134,6 +157,12 @@ export class RBoxApp extends HTMLElement {
|
||||
this.switchView('deleted');
|
||||
});
|
||||
|
||||
shortcuts.register('5', () => {
|
||||
if (this.user && this.user.is_superuser) {
|
||||
this.switchView('admin');
|
||||
}
|
||||
});
|
||||
|
||||
shortcuts.register('f2', () => {
|
||||
console.log('Rename shortcut - to be implemented');
|
||||
});
|
||||
@@ -176,6 +205,11 @@ export class RBoxApp extends HTMLElement {
|
||||
<kbd>4</kbd>
|
||||
<span>Deleted Files</span>
|
||||
</div>
|
||||
${this.user && this.user.is_superuser ? `
|
||||
<div class="shortcut-item">
|
||||
<kbd>5</kbd>
|
||||
<span>Admin Dashboard</span>
|
||||
</div>` : ''}
|
||||
|
||||
<h3>General</h3>
|
||||
<div class="shortcut-item">
|
||||
@@ -329,16 +363,24 @@ export class RBoxApp extends HTMLElement {
|
||||
this.attachListeners();
|
||||
break;
|
||||
case 'shared':
|
||||
mainContent.innerHTML = '<div class="placeholder">Shared Items - Coming Soon</div>';
|
||||
mainContent.innerHTML = '<shared-items></shared-items>';
|
||||
this.attachListeners();
|
||||
break;
|
||||
case 'deleted':
|
||||
mainContent.innerHTML = '<div class="placeholder">Deleted Files - Coming Soon</div>';
|
||||
mainContent.innerHTML = '<deleted-files></deleted-files>';
|
||||
this.attachListeners(); // Re-attach listeners for the new component
|
||||
break;
|
||||
case 'starred':
|
||||
mainContent.innerHTML = '<div class="placeholder">Starred Items - Coming Soon</div>';
|
||||
mainContent.innerHTML = '<starred-items></starred-items>';
|
||||
this.attachListeners();
|
||||
break;
|
||||
case 'recent':
|
||||
mainContent.innerHTML = '<div class="placeholder">Recent Files - Coming Soon</div>';
|
||||
mainContent.innerHTML = '<recent-files></recent-files>';
|
||||
this.attachListeners();
|
||||
break;
|
||||
case 'admin':
|
||||
mainContent.innerHTML = '<admin-dashboard></admin-dashboard>';
|
||||
this.attachListeners();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { api } from '../api.js';
|
||||
|
||||
export class RecentFiles extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.recentFiles = [];
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await this.loadRecentFiles();
|
||||
}
|
||||
|
||||
async loadRecentFiles() {
|
||||
try {
|
||||
this.recentFiles = await api.listRecentFiles();
|
||||
this.render();
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent files:', error);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to load recent files: ' + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.recentFiles.length === 0) {
|
||||
this.innerHTML = `
|
||||
<div class="recent-files-container">
|
||||
<h2>Recent Files</h2>
|
||||
<p class="empty-state">No recent files found.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
this.innerHTML = `
|
||||
<div class="recent-files-container">
|
||||
<h2>Recent Files</h2>
|
||||
<div class="file-grid">
|
||||
${this.recentFiles.map(file => this.renderFile(file)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.attachListeners();
|
||||
}
|
||||
|
||||
renderFile(file) {
|
||||
const icon = this.getFileIcon(file.mime_type);
|
||||
const size = this.formatFileSize(file.size);
|
||||
const lastAccessed = file.last_accessed_at ? new Date(file.last_accessed_at).toLocaleString() : 'N/A';
|
||||
|
||||
return `
|
||||
<div class="file-item" data-file-id="${file.id}">
|
||||
<div class="file-icon">${icon}</div>
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-size">${size}</div>
|
||||
<div class="file-last-accessed">Accessed: ${lastAccessed}</div>
|
||||
<div class="file-actions-menu">
|
||||
<button class="action-btn" data-action="download" data-id="${file.id}">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getFileIcon(mimeType) {
|
||||
if (mimeType.startsWith('image/')) return '📷';
|
||||
if (mimeType.startsWith('video/')) return '🎥';
|
||||
if (mimeType.startsWith('audio/')) return '🎵';
|
||||
if (mimeType.includes('pdf')) return '📄';
|
||||
if (mimeType.includes('text')) return '📄';
|
||||
return '📄';
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
return (bytes / 1073741824).toFixed(1) + ' GB';
|
||||
}
|
||||
|
||||
attachListeners() {
|
||||
this.querySelectorAll('.action-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const action = btn.dataset.action;
|
||||
const id = parseInt(btn.dataset.id);
|
||||
if (action === 'download') {
|
||||
await this.handleDownload(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async handleDownload(fileId) {
|
||||
try {
|
||||
const blob = await api.downloadFile(fileId);
|
||||
const file = this.recentFiles.find(f => f.id === fileId);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = file.name;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'File downloaded successfully!', type: 'success' }
|
||||
}));
|
||||
} catch (error) {
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to download file: ' + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('recent-files', RecentFiles);
|
||||
@@ -126,7 +126,9 @@ export class ShareModal extends HTMLElement {
|
||||
const linkInput = this.querySelector('#share-link');
|
||||
linkInput.select();
|
||||
document.execCommand('copy');
|
||||
alert('Link copied to clipboard');
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Link copied to clipboard!', type: 'success' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { api } from '../api.js';
|
||||
|
||||
export class SharedItems extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.myShares = [];
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await this.loadMyShares();
|
||||
}
|
||||
|
||||
async loadMyShares() {
|
||||
try {
|
||||
this.myShares = await api.listMyShares();
|
||||
this.render();
|
||||
} catch (error) {
|
||||
console.error('Failed to load shared items:', error);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to load shared items: ' + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.myShares.length === 0) {
|
||||
this.innerHTML = `
|
||||
<div class="shared-items-container">
|
||||
<h2>Shared Items</h2>
|
||||
<p class="empty-state">No shared items found.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
this.innerHTML = `
|
||||
<div class="shared-items-container">
|
||||
<h2>Shared Items</h2>
|
||||
<div class="share-list">
|
||||
${this.myShares.map(share => this.renderShare(share)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.attachListeners();
|
||||
}
|
||||
|
||||
renderShare(share) {
|
||||
const shareLink = `${window.location.origin}/share/${share.token}`;
|
||||
const expiresAt = share.expires_at ? new Date(share.expires_at).toLocaleString() : 'Never';
|
||||
const targetName = share.file ? share.file.name : (share.folder ? share.folder.name : 'N/A');
|
||||
const targetType = share.file ? 'File' : (share.folder ? 'Folder' : 'N/A');
|
||||
|
||||
return `
|
||||
<div class="share-item" data-share-id="${share.id}">
|
||||
<div class="share-info">
|
||||
<p><strong>${targetType}:</strong> ${targetName}</p>
|
||||
<p><strong>Permission:</strong> ${share.permission_level}</p>
|
||||
<p><strong>Expires:</strong> ${expiresAt}</p>
|
||||
<p><strong>Password Protected:</strong> ${share.password_protected ? 'Yes' : 'No'}</p>
|
||||
<p><strong>Access Count:</strong> ${share.access_count}</p>
|
||||
<input type="text" value="${shareLink}" readonly class="input-field share-link-input">
|
||||
</div>
|
||||
<div class="share-actions">
|
||||
<button class="button button-small" data-action="copy-link" data-link="${shareLink}">Copy Link</button>
|
||||
<button class="button button-small" data-action="edit-share" data-id="${share.id}">Edit</button>
|
||||
<button class="button button-small button-danger" data-action="delete-share" data-id="${share.id}">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
attachListeners() {
|
||||
this.querySelectorAll('.share-actions .button').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const action = btn.dataset.action;
|
||||
const id = parseInt(btn.dataset.id);
|
||||
const link = btn.dataset.link;
|
||||
|
||||
if (action === 'copy-link') {
|
||||
this.copyLink(link);
|
||||
} else if (action === 'edit-share') {
|
||||
this.handleEditShare(id);
|
||||
} else if (action === 'delete-share') {
|
||||
await this.handleDeleteShare(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
copyLink(link) {
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Share link copied to clipboard!', type: 'success' }
|
||||
}));
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy link: ', err);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to copy link.', type: 'error' }
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
handleEditShare(shareId) {
|
||||
// For now, we'll just show a toast. A full implementation would open a modal for editing.
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: `Edit share ${shareId} - functionality to be implemented.`, type: 'info' }
|
||||
}));
|
||||
}
|
||||
|
||||
async handleDeleteShare(shareId) {
|
||||
if (confirm('Are you sure you want to delete this share link?')) {
|
||||
try {
|
||||
await api.deleteShare(shareId);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Share link deleted successfully!', type: 'success' }
|
||||
}));
|
||||
await this.loadMyShares(); // Reload the list
|
||||
} catch (error) {
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to delete share link: ' + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('shared-items', SharedItems);
|
||||
@@ -0,0 +1,147 @@
|
||||
import { api } from '../api.js';
|
||||
|
||||
export class StarredItems extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.starredFiles = [];
|
||||
this.starredFolders = [];
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await this.loadStarredItems();
|
||||
}
|
||||
|
||||
async loadStarredItems() {
|
||||
try {
|
||||
this.starredFiles = await api.listStarredFiles();
|
||||
this.starredFolders = await api.listStarredFolders();
|
||||
this.render();
|
||||
} catch (error) {
|
||||
console.error('Failed to load starred items:', error);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to load starred items: ' + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const allStarred = [...this.starredFolders, ...this.starredFiles];
|
||||
|
||||
if (allStarred.length === 0) {
|
||||
this.innerHTML = `
|
||||
<div class="starred-items-container">
|
||||
<h2>Starred Items</h2>
|
||||
<p class="empty-state">No starred items found.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
this.innerHTML = `
|
||||
<div class="starred-items-container">
|
||||
<h2>Starred Items</h2>
|
||||
<div class="file-grid">
|
||||
${this.starredFolders.map(folder => this.renderFolder(folder)).join('')}
|
||||
${this.starredFiles.map(file => this.renderFile(file)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.attachListeners();
|
||||
}
|
||||
|
||||
renderFolder(folder) {
|
||||
return `
|
||||
<div class="file-item folder-item" data-folder-id="${folder.id}">
|
||||
<div class="file-icon">📁</div>
|
||||
<div class="file-name">${folder.name}</div>
|
||||
<div class="file-actions-menu">
|
||||
<button class="action-btn star-btn" data-action="unstar-folder" data-id="${folder.id}">★</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderFile(file) {
|
||||
const icon = this.getFileIcon(file.mime_type);
|
||||
const size = this.formatFileSize(file.size);
|
||||
|
||||
return `
|
||||
<div class="file-item" data-file-id="${file.id}">
|
||||
<div class="file-icon">${icon}</div>
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-size">${size}</div>
|
||||
<div class="file-actions-menu">
|
||||
<button class="action-btn" data-action="download" data-id="${file.id}">Download</button>
|
||||
<button class="action-btn star-btn" data-action="unstar-file" data-id="${file.id}">★</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getFileIcon(mimeType) {
|
||||
if (mimeType.startsWith('image/')) return '📷';
|
||||
if (mimeType.startsWith('video/')) return '🎥';
|
||||
if (mimeType.startsWith('audio/')) return '🎵';
|
||||
if (mimeType.includes('pdf')) return '📄';
|
||||
if (mimeType.includes('text')) return '📄';
|
||||
return '📄';
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
return (bytes / 1073741824).toFixed(1) + ' GB';
|
||||
}
|
||||
|
||||
attachListeners() {
|
||||
this.querySelectorAll('.action-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const action = btn.dataset.action;
|
||||
const id = parseInt(btn.dataset.id);
|
||||
await this.handleAction(action, id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async handleAction(action, id) {
|
||||
try {
|
||||
switch (action) {
|
||||
case 'download':
|
||||
const blob = await api.downloadFile(id);
|
||||
const file = this.starredFiles.find(f => f.id === id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = file.name;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'File downloaded successfully!', type: 'success' }
|
||||
}));
|
||||
break;
|
||||
case 'unstar-file':
|
||||
await api.unstarFile(id);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'File unstarred successfully!', type: 'success' }
|
||||
}));
|
||||
await this.loadStarredItems();
|
||||
break;
|
||||
case 'unstar-folder':
|
||||
await api.unstarFolder(id);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Folder unstarred successfully!', type: 'success' }
|
||||
}));
|
||||
await this.loadStarredItems();
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Action failed: ' + error.message, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('starred-items', StarredItems);
|
||||
@@ -0,0 +1,76 @@
|
||||
export class ToastNotification extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--primary-color);
|
||||
color: var(--accent-color);
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
font-family: var(--font-family);
|
||||
font-size: 1rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||
z-index: 10000;
|
||||
min-width: 250px;
|
||||
text-align: center;
|
||||
}
|
||||
:host(.show) {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
:host(.hide) {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
}
|
||||
:host(.error) {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
:host(.success) {
|
||||
background-color: #4CAF50; /* Green for success */
|
||||
}
|
||||
</style>
|
||||
<div id="message"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Ensure variables are defined, fallback if not
|
||||
if (!this.style.getPropertyValue('--primary-color')) {
|
||||
this.style.setProperty('--primary-color', '#003399');
|
||||
this.style.setProperty('--secondary-color', '#CC0000');
|
||||
this.style.setProperty('--accent-color', '#FFFFFF');
|
||||
this.style.setProperty('--font-family', 'sans-serif');
|
||||
}
|
||||
}
|
||||
|
||||
show(message, type = 'info', duration = 3000) {
|
||||
const messageDiv = this.shadowRoot.getElementById('message');
|
||||
messageDiv.textContent = message;
|
||||
|
||||
this.className = ''; // Clear previous classes
|
||||
this.classList.add('show');
|
||||
if (type === 'error') {
|
||||
this.classList.add('error');
|
||||
} else if (type === 'success') {
|
||||
this.classList.add('success');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.classList.remove('show');
|
||||
this.classList.add('hide');
|
||||
// Remove element after transition
|
||||
this.addEventListener('transitionend', () => this.remove(), { once: true });
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('toast-notification', ToastNotification);
|
||||
Reference in New Issue
Block a user