/** * OxiCloud - Shared View Component * In-app shared files view. All operations go through the backend API. */ import { switchToFilesSection } from '../../app/navigation.js'; import { ui } from '../../app/ui.js'; import { getCsrfHeaders } from '../../core/csrf.js'; import { formatDateShort } from '../../core/formatters.js'; import { i18n } from '../../core/i18n.js'; import { fileSharing } from '../../features/sharing/fileSharing.js'; const sharedView = { // State items: [], filteredItems: [], currentItem: null, /** Auth header helper — tokens are in HttpOnly cookies now */ _headers(json = false) { const h = { ...getCsrfHeaders() }; if (json) h['Content-Type'] = 'application/json'; return h; }, init() { console.log('Initializing shared view component (API-backed)'); this.loadItems(); }, show() { this.displayUI(); this.attachEventListeners(); this.loadItems().then(() => this.filterAndSortItems()); const c = document.getElementById('shared-container'); if (c) c.classList.remove('hidden'); }, hide() { const c = document.getElementById('shared-container'); if (c) c.classList.add('hidden'); }, // Load shared items from backend API async loadItems() { try { const res = await fetch('/api/shares?page=1&per_page=1000', { headers: this._headers() }); if (res.ok) { const data = await res.json(); this.items = data.items || []; } else { this.items = []; } } catch (err) { console.error('Error loading shared items:', err); this.items = []; } this.filteredItems = [...this.items]; }, // Create and display the shared view UI displayUI() { const contentArea = document.querySelector('.content-area'); let container = document.getElementById('shared-container'); if (!container) { container = document.createElement('div'); container.id = 'shared-container'; container.className = 'shared-view-container'; if (contentArea) contentArea.appendChild(container); } container.innerHTML = `
All
Files
Folders
Sort by date
Sort by name
Sort by expiration
📄 Item
📧 Item
`; if (i18n?.translateElement) { i18n.translateElement(container); } }, // Attach event listeners attachEventListeners() { // Custom dropdown logic for filter-type this._initCustomSelect('filter-type-wrapper', 'filter-type-toggle', 'filter-type-dropdown'); // Custom dropdown logic for sort-by this._initCustomSelect('sort-by-wrapper', 'sort-by-toggle', 'sort-by-dropdown'); // Close dropdowns when clicking outside document.addEventListener('click', (e) => { document.querySelectorAll('.shared-custom-select.open').forEach((sel) => { if (!sel.contains(e.target)) sel.classList.remove('open'); }); }); // Share dialog (sharedView-specific IDs) const shareDialog = document.getElementById('shared-view-edit-dialog'); if (shareDialog) { const closeBtn = shareDialog.querySelector('.close-dialog-btn'); if (closeBtn) closeBtn.addEventListener('click', () => this.closeShareDialog()); const copyLinkBtn = document.getElementById('sv-copy-link-btn'); if (copyLinkBtn) copyLinkBtn.addEventListener('click', () => this.copyShareLink()); const enablePw = document.getElementById('sv-enable-password'); const pwField = document.getElementById('sv-share-password'); if (enablePw) enablePw.addEventListener('change', () => { if (pwField) { pwField.disabled = !enablePw.checked; if (enablePw.checked) pwField.focus(); } }); const genPwBtn = document.getElementById('sv-generate-password'); if (genPwBtn) genPwBtn.addEventListener('click', () => this.generatePassword()); const enableExp = document.getElementById('sv-enable-expiration'); const expField = document.getElementById('sv-share-expiration'); if (enableExp) enableExp.addEventListener('change', () => { if (expField) { expField.disabled = !enableExp.checked; if (enableExp.checked) expField.focus(); } }); const updateBtn = document.getElementById('sv-update-share-btn'); if (updateBtn) updateBtn.addEventListener('click', () => this.updateSharedItem()); const removeBtn = document.getElementById('sv-remove-share-btn'); if (removeBtn) removeBtn.addEventListener('click', () => this.removeSharedItem()); } // Notification dialog (sharedView-specific IDs) const notifDialog = document.getElementById('sv-notification-dialog'); if (notifDialog) { const closeBtn = notifDialog.querySelector('.close-dialog-btn'); if (closeBtn) closeBtn.addEventListener('click', () => this.closeNotificationDialog()); const sendBtn = document.getElementById('sv-send-notification-btn'); if (sendBtn) sendBtn.addEventListener('click', () => this.sendNotification()); } // "Go to Files" button in empty state const goToFilesBtn = document.getElementById('go-to-files-btn'); if (goToFilesBtn) { goToFilesBtn.addEventListener('click', () => { if (switchToFilesSection) switchToFilesSection(); }); } }, // Initialize a custom select dropdown _initCustomSelect(wrapperId, toggleId, dropdownId) { const wrapper = document.getElementById(wrapperId); const toggle = document.getElementById(toggleId); const dropdown = document.getElementById(dropdownId); if (!wrapper || !toggle || !dropdown) return; toggle.addEventListener('click', (e) => { e.stopPropagation(); // Close other open selects document.querySelectorAll('.shared-custom-select.open').forEach((sel) => { if (sel !== wrapper) sel.classList.remove('open'); }); wrapper.classList.toggle('open'); }); dropdown.querySelectorAll('.shared-select-option').forEach((option) => { option.addEventListener('click', (e) => { e.stopPropagation(); // Update active state dropdown.querySelectorAll('.shared-select-option').forEach((o) => { o.classList.remove('active'); }); option.classList.add('active'); // Update label const label = toggle.querySelector('.shared-select-label'); if (label) label.textContent = option.textContent; // Close dropdown wrapper.classList.remove('open'); // Trigger filter this.filterAndSortItems(); }); }); }, // Filter and sort items filterAndSortItems() { const filterTypeActive = document.querySelector('#filter-type-dropdown .shared-select-option.active'); const sortByActive = document.querySelector('#sort-by-dropdown .shared-select-option.active'); const type = filterTypeActive ? filterTypeActive.dataset.value : 'all'; const sort = sortByActive ? sortByActive.dataset.value : 'date'; // Use the main top-bar search input const searchInput = document.getElementById('search-input'); const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; this.filteredItems = this.items.filter((item) => { if (type !== 'all' && item.item_type !== type) return false; const name = (item.item_name || item.item_id || '').toLowerCase(); return name.includes(searchTerm); }); this.filteredItems.sort((a, b) => { if (sort === 'name') { return (a.item_name || a.item_id || '').localeCompare(b.item_name || b.item_id || ''); } else if (sort === 'date') { return (b.created_at || 0) - (a.created_at || 0); } else if (sort === 'expiration') { if (!a.expires_at && !b.expires_at) return 0; if (!a.expires_at) return 1; if (!b.expires_at) return -1; return a.expires_at - b.expires_at; } return 0; }); this.displaySharedItems(); }, // Display items in the table displaySharedItems() { const sharedItemsList = document.getElementById('shared-items-list'); const emptyState = document.getElementById('empty-shared-state'); const listContainer = document.querySelector('.shared-list-container'); if (!sharedItemsList || !emptyState || !listContainer) return; sharedItemsList.innerHTML = ''; if (this.filteredItems.length === 0) { emptyState.classList.remove('hidden'); listContainer.classList.add('hidden'); return; } emptyState.classList.add('hidden'); listContainer.classList.remove('hidden'); this.filteredItems.forEach((item) => { const row = document.createElement('tr'); const displayName = item.item_name || item.item_id || 'Unknown'; const nameCell = document.createElement('td'); nameCell.className = 'shared-item-name'; const iconSpan = document.createElement('span'); iconSpan.className = 'item-icon'; iconSpan.textContent = item.item_type === 'file' ? '📄' : '📁'; const nameSpan = document.createElement('span'); nameSpan.textContent = displayName; nameCell.appendChild(iconSpan); nameCell.appendChild(nameSpan); const typeCell = document.createElement('td'); typeCell.textContent = item.item_type === 'file' ? this.translate('shared_typeFile', 'File') : this.translate('shared_typeFolder', 'Folder'); const dateCell = document.createElement('td'); dateCell.textContent = this.formatDate(item.created_at); const expCell = document.createElement('td'); expCell.textContent = item.expires_at ? this.formatDate(item.expires_at) : this.translate('shared_noExpiration', 'No expiration'); const permCell = document.createElement('td'); const perms = []; if (item.permissions?.read) perms.push(this.translate('share_permissionRead', 'Read')); if (item.permissions?.write) perms.push(this.translate('share_permissionWrite', 'Write')); if (item.permissions?.reshare) perms.push(this.translate('share_permissionReshare', 'Reshare')); permCell.textContent = perms.join(', ') || 'Read'; const pwCell = document.createElement('td'); pwCell.textContent = item.has_password ? this.translate('shared_hasPassword', 'Yes') : this.translate('shared_noPassword', 'No'); const actionsCell = document.createElement('td'); actionsCell.className = 'shared-item-actions'; const editBtn = document.createElement('button'); editBtn.className = 'action-btn edit-btn'; editBtn.innerHTML = '✏️'; editBtn.title = this.translate('shared_editShare', 'Edit Share'); editBtn.addEventListener('click', () => this.openShareDialog(item)); const notifyBtn = document.createElement('button'); notifyBtn.className = 'action-btn notify-btn'; notifyBtn.innerHTML = '📧'; notifyBtn.title = this.translate('shared_notifyShare', 'Notify Someone'); notifyBtn.addEventListener('click', () => this.openNotificationDialog(item)); const copyBtn = document.createElement('button'); copyBtn.className = 'action-btn copy-btn'; copyBtn.innerHTML = '📋'; copyBtn.title = this.translate('shared_copyLink', 'Copy Link'); copyBtn.addEventListener('click', () => { navigator.clipboard .writeText(item.url) .then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!'))) .catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error')); }); const rmBtn = document.createElement('button'); rmBtn.className = 'action-btn remove-btn'; rmBtn.innerHTML = '🗑️'; rmBtn.title = this.translate('shared_removeShare', 'Remove Share'); rmBtn.addEventListener('click', () => { this.currentItem = item; this.removeSharedItem(); }); actionsCell.append(editBtn, notifyBtn, copyBtn, rmBtn); row.append(nameCell, typeCell, dateCell, expCell, permCell, pwCell, actionsCell); sharedItemsList.appendChild(row); }); }, // Open share dialog openShareDialog(item) { this.currentItem = item; const shareDialog = document.getElementById('shared-view-edit-dialog'); const dn = item.item_name || item.item_id || 'Unknown'; const iconEl = document.getElementById('sv-dialog-icon'); const nameEl = document.getElementById('sv-dialog-name'); const urlEl = document.getElementById('sv-share-link-url'); const enablePw = document.getElementById('sv-enable-password'); const pwField = document.getElementById('sv-share-password'); const enableExp = document.getElementById('sv-enable-expiration'); const expField = document.getElementById('sv-share-expiration'); const permRead = document.getElementById('sv-permission-read'); const permWrite = document.getElementById('sv-permission-write'); const permReshare = document.getElementById('sv-permission-reshare'); if (!shareDialog) return; if (iconEl) iconEl.textContent = item.item_type === 'file' ? '📄' : '📁'; if (nameEl) nameEl.textContent = dn; if (urlEl) urlEl.value = item.url || ''; if (permRead) permRead.checked = item.permissions?.read !== false; if (permWrite) permWrite.checked = !!item.permissions?.write; if (permReshare) permReshare.checked = !!item.permissions?.reshare; if (enablePw) { enablePw.checked = item.has_password; if (pwField) { pwField.disabled = !enablePw.checked; pwField.value = ''; } } if (enableExp) { enableExp.checked = !!item.expires_at; if (expField) { expField.disabled = !enableExp.checked; expField.value = item.expires_at ? new Date(item.expires_at * 1000).toISOString().split('T')[0] : ''; } } shareDialog.classList.add('active'); }, closeShareDialog() { const d = document.getElementById('shared-view-edit-dialog'); if (d) d.classList.remove('active'); this.currentItem = null; }, openNotificationDialog(item) { this.currentItem = item; const dn = item.item_name || item.item_id || 'Unknown'; const d = document.getElementById('sv-notification-dialog'); const iconEl = document.getElementById('sv-notify-dialog-icon'); const nameEl = document.getElementById('sv-notify-dialog-name'); const emailEl = document.getElementById('sv-notification-email'); const msgEl = document.getElementById('sv-notification-message'); if (!d) return; if (iconEl) iconEl.textContent = item.item_type === 'file' ? '📄' : '📁'; if (nameEl) nameEl.textContent = dn; if (emailEl) emailEl.value = ''; if (msgEl) msgEl.value = ''; d.classList.add('active'); }, closeNotificationDialog() { const d = document.getElementById('sv-notification-dialog'); if (d) d.classList.remove('active'); this.currentItem = null; }, copyShareLink() { const el = document.getElementById('sv-share-link-url'); if (!el) return; navigator.clipboard .writeText(el.value) .then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!'))) .catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error')); }, // Generate secure password with crypto API generatePassword() { const pwField = document.getElementById('sv-share-password'); const enablePw = document.getElementById('sv-enable-password'); if (!pwField || !enablePw) return; const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'; const array = new Uint32Array(16); crypto.getRandomValues(array); let password = ''; for (let i = 0; i < 16; i++) { password += chars[array[i] % chars.length]; } pwField.value = password; enablePw.checked = true; pwField.disabled = false; }, // Update share via API async updateSharedItem() { if (!this.currentItem) return; const permRead = document.getElementById('sv-permission-read'); const permWrite = document.getElementById('sv-permission-write'); const permReshare = document.getElementById('sv-permission-reshare'); const enablePw = document.getElementById('sv-enable-password'); const pwField = document.getElementById('sv-share-password'); const enableExp = document.getElementById('sv-enable-expiration'); const expField = document.getElementById('sv-share-expiration'); const body = { permissions: { read: permRead ? permRead.checked : true, write: permWrite ? permWrite.checked : false, reshare: permReshare ? permReshare.checked : false }, password: enablePw?.checked && pwField?.value ? pwField.value : null, expires_at: enableExp?.checked && expField?.value ? Math.floor(new Date(expField.value).getTime() / 1000) : null }; try { const res = await fetch(`/api/shares/${this.currentItem.id}`, { method: 'PUT', headers: this._headers(true), body: JSON.stringify(body) }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || `Server error ${res.status}`); } this.showNotification(this.translate('shared_itemUpdated', 'Share settings updated')); } catch (err) { console.error('Error updating share:', err); this.showNotification(err.message || 'Error updating share', 'error'); } this.closeShareDialog(); await this.loadItems(); this.filterAndSortItems(); }, // Remove share via API async removeSharedItem() { if (!this.currentItem) return; try { const res = await fetch(`/api/shares/${this.currentItem.id}`, { method: 'DELETE', headers: this._headers() }); if (!res.ok && res.status !== 204) throw new Error(`Server error ${res.status}`); this.showNotification(this.translate('shared_itemRemoved', 'Share removed')); } catch (err) { console.error('Error removing share:', err); this.showNotification('Error removing share', 'error'); } this.closeShareDialog(); await this.loadItems(); this.filterAndSortItems(); }, // Send notification (stub) sendNotification() { if (!this.currentItem) return; const emailEl = document.getElementById('sv-notification-email'); const msgEl = document.getElementById('sv-notification-message'); const email = emailEl ? emailEl.value.trim() : ''; const message = msgEl ? msgEl.value.trim() : ''; if (!email || !this.validateEmail(email)) { this.showNotification(this.translate('shared_invalidEmail', 'Please enter a valid email address'), 'error'); return; } if (fileSharing?.sendShareNotification) { fileSharing .sendShareNotification(this.currentItem.url, email, message) .then(() => { this.closeNotificationDialog(); this.showNotification(this.translate('shared_notificationSent', 'Notification sent')); }) .catch(() => this.showNotification(this.translate('shared_notificationFailed', 'Failed to send notification'), 'error')); } }, showNotification(message, type = 'success') { if (ui?.showNotification) { ui.showNotification(message, type); } else { alert(message); } }, validateEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }, formatDate(value) { return formatDateShort(value); }, translate(key, defaultText) { if (i18n?.t) return i18n.t(key, defaultText); return defaultText; } }; export { sharedView };