This commit is contained in:
2025-11-10 01:58:41 +01:00
parent 17de53b9c2
commit 1ddb2c609d
21 changed files with 1438 additions and 30 deletions
+109
View File
@@ -171,6 +171,25 @@ body {
background-color: #AA0000;
}
.button-danger {
background-color: var(--secondary-color);
color: var(--accent-color);
}
.button-danger:hover {
background-color: #AA0000;
}
.deleted-item {
border-color: var(--secondary-color);
background-color: rgba(255, 0, 0, 0.05); /* Light red tint */
}
.deleted-item .file-name,
.deleted-item .file-deleted-date {
color: var(--secondary-color);
}
.input-field {
border: 1px solid var(--border-color);
padding: var(--spacing-unit);
@@ -786,3 +805,93 @@ body.dark-mode {
margin: 0 0 calc(var(--spacing-unit) * 2) 0;
color: var(--primary-color);
}
.admin-dashboard-container {
background-color: var(--accent-color);
border-radius: 8px;
padding: calc(var(--spacing-unit) * 2);
}
.admin-dashboard-container h2 {
margin: 0 0 calc(var(--spacing-unit) * 2) 0;
color: var(--primary-color);
border-bottom: 1px solid var(--border-color);
padding-bottom: calc(var(--spacing-unit) * 2);
}
.admin-section {
margin-bottom: calc(var(--spacing-unit) * 3);
}
.admin-section h3 {
margin: 0 0 calc(var(--spacing-unit) * 1.5) 0;
color: var(--text-color);
font-size: 1.2rem;
}
.user-list {
display: flex;
flex-direction: column;
gap: var(--spacing-unit);
}
.user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--background-color);
}
.user-actions {
display: flex;
gap: var(--spacing-unit);
}
.button-small {
padding: calc(var(--spacing-unit) / 2) var(--spacing-unit);
font-size: 0.8rem;
}
.star-btn {
color: gold; /* Gold color for stars */
border: none;
background: none;
font-size: 1.2rem;
cursor: pointer;
padding: 0;
margin-left: var(--spacing-unit);
}
.star-btn:hover {
transform: scale(1.1);
}
.batch-actions {
display: flex;
align-items: center;
gap: var(--spacing-unit);
background-color: var(--background-color);
padding: var(--spacing-unit);
border-radius: 8px;
margin-bottom: calc(var(--spacing-unit) * 2);
flex-wrap: wrap;
}
.batch-actions label {
font-weight: 500;
margin-right: var(--spacing-unit);
}
.select-item {
margin-right: var(--spacing-unit);
}
.file-item .select-item {
position: absolute;
top: var(--spacing-unit);
left: var(--spacing-unit);
z-index: 1;
}
+109
View File
@@ -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 '&#128247;';
if (mimeType.startsWith('video/')) return '&#127909;';
if (mimeType.startsWith('audio/')) return '&#127925;';
if (mimeType.includes('pdf')) return '&#128196;';
if (mimeType.includes('text')) return '&#128196;';
return '&#128196;';
}
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);
+187 -3
View File
@@ -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 ? '&#9733;' : '&#9734;'; // 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">&#128193;</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 ? '&#9733;' : '&#9734;'; // 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' }
}));
}
}
}
+46 -4
View File
@@ -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;
}
}
+115
View File
@@ -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 '&#128247;';
if (mimeType.startsWith('video/')) return '&#127909;';
if (mimeType.startsWith('audio/')) return '&#127925;';
if (mimeType.includes('pdf')) return '&#128196;';
if (mimeType.includes('text')) return '&#128196;';
return '&#128196;';
}
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);
+3 -1
View File
@@ -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' }
}));
}
}
+128
View File
@@ -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);
+147
View File
@@ -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">&#128193;</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}">&#9733;</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}">&#9733;</button>
</div>
</div>
`;
}
getFileIcon(mimeType) {
if (mimeType.startsWith('image/')) return '&#128247;';
if (mimeType.startsWith('video/')) return '&#127909;';
if (mimeType.startsWith('audio/')) return '&#127925;';
if (mimeType.includes('pdf')) return '&#128196;';
if (mimeType.includes('text')) return '&#128196;';
return '&#128196;';
}
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);