/** * OxiCloud - Shared Resources Page (/shared) * All operations go through the backend API at /api/shares. */ // Authentication check function checkAuthentication() { const token = localStorage.getItem('oxicloud_token'); const tokenExpiry = localStorage.getItem('oxicloud_token_expiry'); if (!token || !tokenExpiry || new Date(tokenExpiry) < new Date()) { window.location.href = '/login'; } } document.addEventListener('DOMContentLoaded', async () => { // ── i18n ── (translatePage is called automatically by i18n.js on init) function t(key, fallback) { return (window.i18n && window.i18n.t) ? window.i18n.t(key, fallback) : fallback; } // ── Auth headers helper ── function authHeaders(json = false) { const h = {}; const token = localStorage.getItem('oxicloud_token'); if (token) h['Authorization'] = `Bearer ${token}`; if (json) h['Content-Type'] = 'application/json'; return h; } // ── Elements ── const sharedItemsList = document.getElementById('shared-items-list'); const emptySharedState = document.getElementById('empty-shared-state'); const filterType = document.getElementById('filter-type'); const sortBy = document.getElementById('sort-by'); const sharedSearch = document.getElementById('shared-search'); const sharedSearchBtn = document.getElementById('shared-search-btn'); const goToFilesBtn = document.getElementById('go-to-files'); // Share dialog elements const shareDialog = document.getElementById('share-dialog'); const shareDialogCloseBtn = shareDialog ? shareDialog.querySelector('.close-dialog-btn') : null; const shareDialogIcon = document.getElementById('share-dialog-icon'); const shareDialogName = document.getElementById('share-dialog-name'); const shareLinkUrl = document.getElementById('share-link-url'); const copyLinkBtn = document.getElementById('copy-link-btn'); const enablePassword = document.getElementById('enable-password'); const sharePassword = document.getElementById('share-password'); const generatePasswordBtn = document.getElementById('generate-password'); const enableExpiration = document.getElementById('enable-expiration'); const shareExpiration = document.getElementById('share-expiration'); const permissionRead = document.getElementById('permission-read'); const permissionWrite = document.getElementById('permission-write'); const permissionReshare = document.getElementById('permission-reshare'); const updateShareBtn = document.getElementById('update-share-btn'); const removeShareBtn = document.getElementById('remove-share-btn'); // Notification dialog elements const notificationDialog = document.getElementById('share-notification-dialog'); const notificationCloseBtn = notificationDialog ? notificationDialog.querySelector('.close-dialog-btn') : null; const notifyDialogIcon = document.getElementById('notify-dialog-icon'); const notifyDialogName = document.getElementById('notify-dialog-name'); const notificationEmail = document.getElementById('notification-email'); const notificationMessage = document.getElementById('notification-message'); const sendNotificationBtn = document.getElementById('send-notification-btn'); // Notification banner const notificationBanner = document.getElementById('notification-banner'); const notificationBannerMessage = document.getElementById('notification-message'); const closeNotificationBtn = document.getElementById('close-notification'); // ── State ── let currentSharedItem = null; let allSharedItems = []; let filteredItems = []; // ── Init ── checkAuthentication(); await loadSharedItems(); // ── Event listeners ── if (filterType) filterType.addEventListener('change', filterAndSortItems); if (sortBy) sortBy.addEventListener('change', filterAndSortItems); if (sharedSearchBtn) sharedSearchBtn.addEventListener('click', filterAndSortItems); if (sharedSearch) sharedSearch.addEventListener('keyup', e => { if (e.key === 'Enter') filterAndSortItems(); }); if (goToFilesBtn) goToFilesBtn.addEventListener('click', () => window.location.href = '/'); if (shareDialogCloseBtn) shareDialogCloseBtn.addEventListener('click', closeShareDialog); if (copyLinkBtn) copyLinkBtn.addEventListener('click', copyShareLink); if (enablePassword) enablePassword.addEventListener('change', () => { if (sharePassword) { sharePassword.disabled = !enablePassword.checked; if (enablePassword.checked) sharePassword.focus(); } }); if (generatePasswordBtn) generatePasswordBtn.addEventListener('click', generatePassword); if (enableExpiration) enableExpiration.addEventListener('change', () => { if (shareExpiration) { shareExpiration.disabled = !enableExpiration.checked; if (enableExpiration.checked) shareExpiration.focus(); } }); if (updateShareBtn) updateShareBtn.addEventListener('click', updateSharedItem); if (removeShareBtn) removeShareBtn.addEventListener('click', removeSharedItem); if (notificationCloseBtn) notificationCloseBtn.addEventListener('click', closeNotificationDialog); if (sendNotificationBtn) sendNotificationBtn.addEventListener('click', sendNotification); if (closeNotificationBtn) closeNotificationBtn.addEventListener('click', () => { if (notificationBanner) notificationBanner.classList.remove('active'); }); // ── Load shares from backend ── async function loadSharedItems() { try { const res = await fetch('/api/shares?page=1&per_page=1000', { headers: authHeaders() }); if (res.ok) { const data = await res.json(); allSharedItems = data.items || []; } else { allSharedItems = []; } } catch (err) { console.error('Error loading shared items:', err); allSharedItems = []; } filterAndSortItems(); } // ── Filter & sort ── function filterAndSortItems() { const type = filterType ? filterType.value : 'all'; const sort = sortBy ? sortBy.value : 'date'; const searchTerm = sharedSearch ? sharedSearch.value.toLowerCase() : ''; filteredItems = allSharedItems.filter(item => { if (type !== 'all' && item.item_type !== type) return false; const name = (item.item_name || item.item_id || '').toLowerCase(); return name.includes(searchTerm); }); 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; }); displaySharedItems(); } // ── Display table ── function displaySharedItems() { if (!sharedItemsList) return; sharedItemsList.innerHTML = ''; if (filteredItems.length === 0) { if (emptySharedState) emptySharedState.style.display = 'flex'; const listContainer = document.querySelector('.shared-list-container'); if (listContainer) listContainer.style.display = 'none'; return; } if (emptySharedState) emptySharedState.style.display = 'none'; const listContainer = document.querySelector('.shared-list-container'); if (listContainer) listContainer.style.display = 'block'; filteredItems.forEach(item => { const row = document.createElement('tr'); const displayName = item.item_name || item.item_id || 'Unknown'; // Name const nameCell = document.createElement('td'); nameCell.className = 'shared-item-name'; nameCell.innerHTML = `${item.item_type === 'file' ? '📄' : '📁'}${displayName}`; // Type const typeCell = document.createElement('td'); typeCell.textContent = item.item_type === 'file' ? t('shared_typeFile', 'File') : t('shared_typeFolder', 'Folder'); // Date const dateCell = document.createElement('td'); dateCell.textContent = formatDate(item.created_at); // Expiration const expirationCell = document.createElement('td'); expirationCell.textContent = item.expires_at ? formatDate(item.expires_at) : t('shared_noExpiration', 'No expiration'); // Permissions const permissionsCell = document.createElement('td'); const perms = []; if (item.permissions?.read) perms.push(t('share_permissionRead', 'Read')); if (item.permissions?.write) perms.push(t('share_permissionWrite', 'Write')); if (item.permissions?.reshare) perms.push(t('share_permissionReshare', 'Reshare')); permissionsCell.textContent = perms.join(', ') || 'Read'; // Password const passwordCell = document.createElement('td'); passwordCell.textContent = item.has_password ? t('shared_hasPassword', 'Yes') : t('shared_noPassword', 'No'); // Actions 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 = t('shared_editShare', 'Edit Share'); editBtn.addEventListener('click', () => openShareDialog(item)); const notifyBtn = document.createElement('button'); notifyBtn.className = 'action-btn notify-btn'; notifyBtn.innerHTML = '📧'; notifyBtn.title = t('shared_notifyShare', 'Notify Someone'); notifyBtn.addEventListener('click', () => openNotificationDialog(item)); const cpBtn = document.createElement('button'); cpBtn.className = 'action-btn copy-btn'; cpBtn.innerHTML = '📋'; cpBtn.title = t('shared_copyLink', 'Copy Link'); cpBtn.addEventListener('click', () => { navigator.clipboard.writeText(item.url) .then(() => showNotification(t('shared_linkCopied', 'Link copied!'))) .catch(() => showNotification(t('shared_linkCopyFailed', 'Failed to copy link'), 'error')); }); const rmBtn = document.createElement('button'); rmBtn.className = 'action-btn remove-btn'; rmBtn.innerHTML = '🗑️'; rmBtn.title = t('shared_removeShare', 'Remove Share'); rmBtn.addEventListener('click', () => { currentSharedItem = item; removeSharedItem(); }); actionsCell.append(editBtn, notifyBtn, cpBtn, rmBtn); row.append(nameCell, typeCell, dateCell, expirationCell, permissionsCell, passwordCell, actionsCell); sharedItemsList.appendChild(row); }); } // ── Share dialog ── function openShareDialog(item) { currentSharedItem = item; const dn = item.item_name || item.item_id || 'Unknown'; if (shareDialogIcon) shareDialogIcon.textContent = item.item_type === 'file' ? '📄' : '📁'; if (shareDialogName) shareDialogName.textContent = dn; if (shareLinkUrl) shareLinkUrl.value = item.url || ''; if (permissionRead) permissionRead.checked = item.permissions?.read !== false; if (permissionWrite) permissionWrite.checked = !!item.permissions?.write; if (permissionReshare) permissionReshare.checked = !!item.permissions?.reshare; if (enablePassword) { enablePassword.checked = item.has_password; if (sharePassword) { sharePassword.disabled = !enablePassword.checked; sharePassword.value = ''; } } if (enableExpiration) { enableExpiration.checked = !!item.expires_at; if (shareExpiration) { shareExpiration.disabled = !enableExpiration.checked; shareExpiration.value = item.expires_at ? new Date(item.expires_at * 1000).toISOString().split('T')[0] : ''; } } if (shareDialog) shareDialog.classList.add('active'); } function closeShareDialog() { if (shareDialog) shareDialog.classList.remove('active'); currentSharedItem = null; } // ── Notification dialog ── function openNotificationDialog(item) { currentSharedItem = item; const dn = item.item_name || item.item_id || 'Unknown'; if (notifyDialogIcon) notifyDialogIcon.textContent = item.item_type === 'file' ? '📄' : '📁'; if (notifyDialogName) notifyDialogName.textContent = dn; if (notificationEmail) notificationEmail.value = ''; if (notificationMessage) notificationMessage.value = ''; if (notificationDialog) notificationDialog.classList.add('active'); } function closeNotificationDialog() { if (notificationDialog) notificationDialog.classList.remove('active'); currentSharedItem = null; } function copyShareLink() { if (!shareLinkUrl) return; navigator.clipboard.writeText(shareLinkUrl.value) .then(() => showNotification(t('shared_linkCopied', 'Link copied!'))) .catch(() => showNotification(t('shared_linkCopyFailed', 'Failed to copy link'), 'error')); } // ── Generate secure password with crypto API ── function generatePassword() { if (!sharePassword || !enablePassword) 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]; } sharePassword.value = password; enablePassword.checked = true; sharePassword.disabled = false; } // ── Update share via API ── async function updateSharedItem() { if (!currentSharedItem) return; const body = { permissions: { read: permissionRead ? permissionRead.checked : true, write: permissionWrite ? permissionWrite.checked : false, reshare: permissionReshare ? permissionReshare.checked : false }, password: (enablePassword && enablePassword.checked && sharePassword && sharePassword.value) ? sharePassword.value : null, expires_at: (enableExpiration && enableExpiration.checked && shareExpiration && shareExpiration.value) ? Math.floor(new Date(shareExpiration.value).getTime() / 1000) : null }; try { const res = await fetch(`/api/shares/${currentSharedItem.id}`, { method: 'PUT', headers: authHeaders(true), body: JSON.stringify(body) }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || `Server error ${res.status}`); } showNotification(t('shared_itemUpdated', 'Share settings updated')); } catch (err) { console.error('Error updating share:', err); showNotification(err.message || 'Error updating share', 'error'); } closeShareDialog(); await loadSharedItems(); } // ── Remove share via API ── async function removeSharedItem() { if (!currentSharedItem) return; try { const res = await fetch(`/api/shares/${currentSharedItem.id}`, { method: 'DELETE', headers: authHeaders() }); if (!res.ok && res.status !== 204) throw new Error(`Server error ${res.status}`); showNotification(t('shared_itemRemoved', 'Share removed')); } catch (err) { console.error('Error removing share:', err); showNotification('Error removing share', 'error'); } closeShareDialog(); await loadSharedItems(); } // ── Send notification (stub) ── function sendNotification() { if (!currentSharedItem) return; const email = notificationEmail ? notificationEmail.value.trim() : ''; const message = notificationMessage ? notificationMessage.value.trim() : ''; if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { showNotification(t('shared_invalidEmail', 'Please enter a valid email address'), 'error'); return; } if (window.fileSharing && window.fileSharing.sendShareNotification) { window.fileSharing.sendShareNotification(currentSharedItem.url, email, message) .then(() => { closeNotificationDialog(); showNotification(t('shared_notificationSent', 'Notification sent')); }) .catch(() => showNotification(t('shared_notificationFailed', 'Failed to send notification'), 'error')); } } // ── Helpers ── function showNotification(message, type = 'success') { if (notificationBannerMessage && notificationBanner) { notificationBannerMessage.textContent = message; notificationBanner.className = 'notification-banner active ' + type; setTimeout(() => notificationBanner.classList.remove('active'), 5000); } else if (window.ui && window.ui.showNotification) { window.ui.showNotification(message, type); } else { alert(message); } } function formatDate(value) { return window.formatDateShort ? window.formatDateShort(value) : String(value); } });