From ae4fb7603ab8ef5aef93ca550957f6b1fa65ec3b Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Fri, 29 May 2026 12:54:52 +0200 Subject: [PATCH] refactor(front): remove dead code (old Shared section) --- static/css/views/shared.css | 294 ---------- static/index.html | 2 - static/js/app/main.js | 8 +- static/js/app/navigation.js | 9 +- static/js/components/shareModal.js | 3 +- static/js/core/types.js | 14 +- static/js/features/sharing/fileSharing.js | 10 +- static/js/views/shared/sharedView.js | 672 ---------------------- 8 files changed, 9 insertions(+), 1003 deletions(-) delete mode 100644 static/css/views/shared.css delete mode 100644 static/js/views/shared/sharedView.js diff --git a/static/css/views/shared.css b/static/css/views/shared.css deleted file mode 100644 index 380605b2..00000000 --- a/static/css/views/shared.css +++ /dev/null @@ -1,294 +0,0 @@ -.page-description { - color: var(--color-text-muted); - font-size: 16px; - margin-top: -15px; - margin-bottom: 25px; -} - -.shared-page-container { - max-width: 1280px; - margin: 20px auto; - padding: 0 20px; -} - -.shared-header { - margin-bottom: 25px; -} - -.shared-header h2 { - font-size: 24px; - color: var(--color-text); - margin-bottom: 8px; -} - -.shared-header p { - color: var(--color-text-muted); - font-size: 16px; -} - -/* In-app Shared view (index.html dynamic container) */ -.shared-view-container .shared-header { - margin-bottom: 20px; -} - -.shared-view-container .shared-filters { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 12px; - flex-wrap: wrap; - margin-bottom: 0; - padding: 15px; - background-color: var(--color-bg-subtle); - box-shadow: 0 1px 3px var(--color-shadow-xs); - border-radius: 10px 10px 0 0; - border-bottom: 1px solid var(--color-border-faint); - font-weight: 600; - color: var(--color-text); -} - -.shared-view-container .shared-custom-select { - position: relative; -} - -.shared-view-container .shared-select-toggle { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: 10px 40px 10px 16px; - border-radius: 12px; - border: 2px solid var(--color-border); - background-color: var(--color-bg-hover); - font-size: 14px; - font-weight: 500; - color: var(--color-text-heading); - cursor: pointer; - min-width: 170px; - height: 44px; - transition: all 0.2s ease; - text-align: left; -} - -.shared-view-container .shared-select-toggle:hover { - border-color: var(--color-border-medium); - background-color: var(--color-bg-surface); -} - -.shared-view-container .shared-custom-select.open .shared-select-toggle { - border-color: var(--color-accent); - background-color: var(--color-bg-surface); - box-shadow: 0 0 0 4px var(--color-accent-ring); -} - -.shared-view-container .shared-select-arrow { - position: absolute; - right: 14px; - top: 50%; - transform: translateY(-50%); - font-size: 10px; - color: var(--color-text-muted); - transition: transform 0.2s ease; - pointer-events: none; -} - -.shared-view-container .shared-custom-select.open .shared-select-arrow { - transform: translateY(-50%) rotate(180deg); -} - -.shared-view-container .shared-select-dropdown { - position: absolute; - top: calc(100% + 6px); - left: 0; - min-width: 100%; - background-color: var(--color-bg-surface); - border-radius: 12px; - box-shadow: 0 4px 20px var(--color-shadow-md); - border: 1px solid var(--color-border); - opacity: 0; - visibility: hidden; - transform: translateY(-10px); - transition: all 0.2s ease; - z-index: 1000; - overflow: hidden; -} - -.shared-view-container .shared-custom-select.open .shared-select-dropdown { - opacity: 1; - visibility: visible; - transform: translateY(0); -} - -.shared-view-container .shared-select-option { - display: flex; - align-items: center; - padding: 12px 16px; - cursor: pointer; - font-size: 14px; - font-weight: 400; - color: var(--color-text-secondary); - transition: background-color 0.15s ease; -} - -.shared-view-container .shared-select-option:hover { - background-color: var(--color-bg-alt); -} - -.shared-view-container .shared-select-option.active { - background-color: var(--color-accent-tint); - color: var(--color-accent); - font-weight: 500; -} - -.shared-filters { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0; - flex-wrap: wrap; - gap: 15px; - background-color: var(--color-bg-subtle); - border-radius: 10px 10px 0 0; - padding: 15px; - border-bottom: 1px solid var(--color-border-faint); - box-shadow: 0 1px 3px var(--color-shadow-xs); - font-weight: 600; - color: var(--color-text); -} - -.filter-group { - display: flex; - align-items: center; - gap: 10px; -} - -.filter-group label { - font-size: 14px; - color: var(--color-text-secondary); - font-weight: 500; -} - -.filter-group select { - padding: 8px 12px; - border: 1px solid var(--color-border); - border-radius: 6px; - background-color: var(--color-bg-surface); - font-size: 14px; - min-width: 120px; -} - -.search-box { - display: flex; - gap: 10px; -} - -.search-box input { - padding: 8px 15px; - border: 1px solid var(--color-border); - border-radius: 6px; - font-size: 14px; - width: 250px; -} - -.shared-list-container { - background-color: var(--color-bg-surface); - border-radius: 0 0 10px 10px; - box-shadow: 0 1px 3px var(--color-shadow-xs); - overflow: hidden; - margin-bottom: 25px; -} - -.shared-list, -.shared-table { - width: 100%; - border-collapse: collapse; -} - -.shared-list thead th, -.shared-table thead th { - padding: 15px; - text-align: left; - font-weight: 600; - color: var(--color-text); - background-color: var(--color-bg-subtle); - border-bottom: 1px solid var(--color-border-faint); -} - -.shared-list tbody td, -.shared-table tbody td { - padding: 15px; - border-bottom: 1px solid var(--color-border-xfaint); - vertical-align: middle; - color: var(--color-text); -} - -.shared-item-name { - display: flex; - align-items: center; - gap: 10px; -} - -.shared-item-actions { - display: flex; - gap: 10px; -} - -.shared-list .action-btn, -.shared-table .action-btn { - width: 32px; - height: 32px; - border: 1px solid var(--color-border); - border-radius: 6px; - background-color: var(--color-bg-surface); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s; -} - -.shared-list .action-btn:hover, -.shared-table .action-btn:hover { - background-color: var(--color-item-hover-blue); - border-color: var(--color-info-border-light); -} - -.action-icon { - font-size: 14px; -} - -#empty-shared-state.empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 50px 20px; - background-color: transparent; - border-radius: 0; - box-shadow: none; - text-align: center; - color: var(--color-text-gray); -} - -#empty-shared-state p { - margin-bottom: 10px; - max-width: 400px; -} - -@media (max-width: 768px) { - .shared-filters { - flex-direction: column; - align-items: flex-start; - } - - .shared-list thead th:nth-child(4), - .shared-list thead th:nth-child(5), - .shared-list tbody td:nth-child(4), - .shared-list tbody td:nth-child(5), - .shared-table thead th:nth-child(4), - .shared-table thead th:nth-child(5), - .shared-table tbody td:nth-child(4), - .shared-table tbody td:nth-child(5) { - display: none; - } -} diff --git a/static/index.html b/static/index.html index 204d0884..d5526fb6 100644 --- a/static/index.html +++ b/static/index.html @@ -13,7 +13,6 @@ - @@ -41,7 +40,6 @@ - diff --git a/static/js/app/main.js b/static/js/app/main.js index e991c638..ec71788d 100644 --- a/static/js/app/main.js +++ b/static/js/app/main.js @@ -18,7 +18,6 @@ import { recent } from '../features/library/recent.js'; import { fileSharing } from '../features/sharing/fileSharing.js'; import { grants } from '../model/grants.js'; import { recentView } from '../views/recent/recentView.js'; -import { sharedView } from '../views/shared/sharedView.js'; import { checkAuthentication } from './authSession.js'; import { loadFiles } from './filesView.js'; import { @@ -700,11 +699,8 @@ function setupEventListeners() { if (searchDebounceTimer) clearTimeout(searchDebounceTimer); const query = elements.searchInput?.value.trim(); - // In shared section, filter locally - if (app.currentSection === 'shared' && sharedView) { - sharedView.filterAndSortItems(); - return; - } + // My Shares section does not support in-page search + if (app.currentSection === 'shared') return; if (query) { performSearch(query); diff --git a/static/js/app/navigation.js b/static/js/app/navigation.js index c58cca24..c6d89bba 100644 --- a/static/js/app/navigation.js +++ b/static/js/app/navigation.js @@ -13,7 +13,6 @@ import { photosView } from '../features/library/photos.js'; import { favoritesView } from '../views/favorites/favoritesView.js'; import { mySharesView } from '../views/myShares/mySharesView.js'; import { recentView } from '../views/recent/recentView.js'; -import { sharedView } from '../views/shared/sharedView.js'; import { sharedWithMeView } from '../views/sharedWithMe/sharedWithMeView.js'; import { filesView, loadFiles } from './filesView.js'; import { setActionsBarMode, setGroupByView, syncGroupByMenu } from './main.js'; @@ -166,9 +165,8 @@ function setCurrentSection(section) { appElements.pageTitle.setAttribute('data-i18n', titleKey); } - // Hide sharedView and mySharesView when switching to any other section + // Hide mySharesView when switching to any other section if (section !== 'shared') { - if (sharedView) sharedView.hide(); mySharesView.hide(); } @@ -301,10 +299,7 @@ function switchToFilesSection() { ui.updateBreadcrumb(); if (batchToolbar) batchToolbar.clear(); - // temp solution - sharedView.loadItems().then(() => { - loadFiles(); - }); + loadFiles(); } function switchToFavoritesSection() { diff --git a/static/js/components/shareModal.js b/static/js/components/shareModal.js index dd6635f7..1307b9df 100644 --- a/static/js/components/shareModal.js +++ b/static/js/components/shareModal.js @@ -858,8 +858,7 @@ const shareModal = { const expiresTs = e._draft.expires_at ? Math.floor(new Date(e._draft.expires_at).getTime() / 1000) : null; await fileSharing.updateSharedLink(e.share.id, { password: e._draft.password, - expires_at: expiresTs, - permissions: null + expires_at: expiresTs }); } } diff --git a/static/js/core/types.js b/static/js/core/types.js index 4a533d4c..d89054a8 100644 --- a/static/js/core/types.js +++ b/static/js/core/types.js @@ -46,13 +46,6 @@ * @property {number} sort_date */ -/** - * @typedef {Object} SharePermissions - * @property {boolean} read - * @property {boolean} reshare - * @property {boolean} write - */ - /** * @typedef {Object} ShareItem * @property {number} access_count @@ -64,7 +57,6 @@ * @property {string} item_id * @property {string} item_name * @property {ItemTypeEnum} item_type - * @property {SharePermissions} permissions * @property {string | null} token * @property {string} url */ @@ -76,14 +68,12 @@ * @property {ItemTypeEnum} item_type * @property {string|null} password * @property {number|null} expires_at - timestamp - * @property {SharePermissions|null} permissions */ /** * @typedef {Object} UpdateShare - * @property {string|null} [password] - * @property {number|null} [expires_at] - * @property {SharePermissions|null} [permissions] + * @property {string|null} [password] + * @property {number|null} [expires_at] */ /** diff --git a/static/js/features/sharing/fileSharing.js b/static/js/features/sharing/fileSharing.js index 98117154..5c873a69 100644 --- a/static/js/features/sharing/fileSharing.js +++ b/static/js/features/sharing/fileSharing.js @@ -35,12 +35,7 @@ const fileSharing = { item_name: options.item_name || null, item_type: itemType, password: options.password || null, - expires_at: options.expires_at ? Math.floor(new Date(options.expires_at).getTime() / 1000) : null, - permissions: options.permissions || { - read: true, - write: false, - reshare: false - } + expires_at: options.expires_at ? Math.floor(new Date(options.expires_at).getTime() / 1000) : null }; const res = await fetch('/api/shares', { @@ -113,12 +108,11 @@ const fileSharing = { /** * Update a shared link * @param {string} shareId - * @param {UpdateShare} updateData - { permissions, password, expires_at } + * @param {UpdateShare} updateData - { password, expires_at } * @returns {Promise} Updated ShareDto */ async updateSharedLink(shareId, updateData) { const body = {}; - if (updateData.permissions) body.permissions = updateData.permissions; if (updateData.password !== undefined) body.password = updateData.password; if (updateData.expires_at !== undefined) body.expires_at = updateData.expires_at; diff --git a/static/js/views/shared/sharedView.js b/static/js/views/shared/sharedView.js deleted file mode 100644 index 4fe18e4e..00000000 --- a/static/js/views/shared/sharedView.js +++ /dev/null @@ -1,672 +0,0 @@ -/** - * 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, isEmailValid } from '../../core/formatters.js'; -import { i18n } from '../../core/i18n.js'; -import { fileSharing } from '../../features/sharing/fileSharing.js'; - -/** @import {ShareItem} from '../../core/types.js' */ - -const TTL = 5 * 60 * 1000; // 5 min - -const sharedView = { - // State - - /** @type {Array} */ - items: [], - - _expires: 0, - - /** @type {Map} key = "file:" | "folder:" */ - _knownItemsId: new Map(), - - /** @type {Array} */ - filteredItems: [], - - /** @type {ShareItem | null} */ - 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; - }, - - async init() { - console.log('Initializing shared view component (API-backed)'); - await this.loadItems(); - }, - - show() { - this.displayUI(); - this.attachEventListeners(); - 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'); - }, - - /** - * tells if item_id is shared - * - * @param {string} id the item_id - * @param {string} type folder|file - * @returns {boolean} true if this item is shared - */ - isShared(id, type) { - return this._knownItemsId.has(`${type}:${id}`); - }, - - // Load shared items from backend API, - // TODO cache entries to minimize calls - /** - * load shared items - * - * @param {boolean} force ignore cache - */ - async loadItems(force = false) { - if (this._expires > Date.now() && !force) return; - - 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 = []; - } - this.filteredItems = { ...this.items }; - this._knownItemsId.clear(); - this.items.forEach((item) => { - this._knownItemsId.set(`${item.item_type}:${item.item_id}`, true); - }); - this._expires = Date.now() + TTL; - } catch (err) { - console.error('Error loading shared items:', err); - 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
-
-
-
-
- - - - - - - - - - - `; - - 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 (!(e.target instanceof Node)) return; - 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 = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password')); - const pwField = /** @type {HTMLInputElement} */ (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 = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-expiration')); - const expField = /** @type {HTMLInputElement} */ (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 - /** - * - * @param {string} wrapperId - * @param {string} toggleId - * @param {string} dropdownId - * @returns - */ - _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 = /** @type {HTMLDivElement} */ (document.querySelector('#filter-type-dropdown .shared-select-option.active')); - const sortByActive = /** @type {HTMLDivElement} */ (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 = /** @type {HTMLInputElement} */ (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' ? i18n.t('shared_typeFile', 'File') : i18n.t('shared_typeFolder', 'Folder'); - - const dateCell = document.createElement('td'); - dateCell.textContent = formatDateShort(item.created_at); - - const expCell = document.createElement('td'); - expCell.textContent = item.expires_at ? formatDateShort(item.expires_at) : i18n.t('shared_noExpiration', 'No expiration'); - - const permCell = document.createElement('td'); - const perms = []; - if (item.permissions?.read) perms.push(i18n.t('share_permissionRead', 'Read')); - if (item.permissions?.write) perms.push(i18n.t('share_permissionWrite', 'Write')); - if (item.permissions?.reshare) perms.push(i18n.t('share_permissionReshare', 'Reshare')); - permCell.textContent = perms.join(', ') || 'Read'; - - const pwCell = document.createElement('td'); - pwCell.textContent = item.has_password ? i18n.t('shared_hasPassword', 'Yes') : i18n.t('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 = i18n.t('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 = i18n.t('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 = i18n.t('shared_copyLink', 'Copy Link'); - copyBtn.addEventListener('click', () => { - navigator.clipboard - .writeText(item.url) - .then(() => ui.showNotification(i18n.t('shared_linkCopied', 'Link copied!'), 'success')) - .catch(() => ui.showNotification(i18n.t('shared_linkCopyFailed', 'Failed to copy link'), 'error')); - }); - - const rmBtn = document.createElement('button'); - rmBtn.className = 'action-btn remove-btn'; - rmBtn.innerHTML = '🗑️'; - rmBtn.title = i18n.t('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 - /** - * - * @param {ShareItem} item - * @returns {void} - */ - 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 = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-link-url')); - const enablePw = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password')); - const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password')); - const enableExp = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-expiration')); - const expField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-expiration')); - const permRead = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-read')); - const permWrite = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-write')); - const permReshare = /** @type {HTMLInputElement} */ (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.remove('hidden'); - }, - - closeShareDialog() { - const d = document.getElementById('shared-view-edit-dialog'); - if (d) d.classList.add('hidden'); - this.currentItem = null; - }, - - /** - * - * @param {ShareItem} item - * @returns {void} - */ - 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 = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-email')); - const msgEl = /** @type {HTMLInputElement} */ (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.remove('hidden'); - }, - - closeNotificationDialog() { - const d = document.getElementById('sv-notification-dialog'); - if (d) d.classList.add('hidden'); - this.currentItem = null; - }, - - copyShareLink() { - const el = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-link-url')); - if (!el) return; - navigator.clipboard - .writeText(el.value) - .then(() => ui.showNotification(i18n.t('shared_linkCopied', 'Link copied!'), 'success')) - .catch(() => ui.showNotification(i18n.t('shared_linkCopyFailed', 'Failed to copy link'), 'error')); - }, - - // Generate secure password with crypto API - generatePassword() { - const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password')); - const enablePw = /** @type {HTMLInputElement} */ (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 = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-read')); - const permWrite = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-write')); - const permReshare = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-reshare')); - const enablePw = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password')); - const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password')); - const enableExp = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-expiration')); - const expField = /** @type {HTMLInputElement} */ (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 { - // FIXME: redundance with fileSharing - 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}`); - } - ui.showNotification(i18n.t('shared_itemUpdated', 'Share settings updated'), 'success'); - } catch (err) { - console.error('Error updating share:', err); - ui.showNotification(/** @type {Error} */ (err).message || 'Error updating share', 'error'); - } - // update UI - ui.setSharedVisualState(this.currentItem.item_id, this.currentItem.item_type, true); - this.closeShareDialog(); - await this.loadItems(true); - this.filterAndSortItems(); - }, - - // Remove share via API - async removeSharedItem() { - if (!this.currentItem) return; - - try { - // FIXME: redundance with fileSharing - 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}`); - ui.showNotification(i18n.t('shared_itemRemoved', 'Share removed'), 'success'); - } catch (err) { - console.error('Error removing share:', err); - ui.showNotification('Error removing share', 'error'); - } - - this.closeShareDialog(); - await this.loadItems(true); - this.filterAndSortItems(); - // update UI - ui.setSharedVisualState(this.currentItem.item_id, this.currentItem.item_type, this.isShared(this.currentItem.item_id, this.currentItem.item_type)); - }, - - // Send notification (stub) - sendNotification() { - if (!this.currentItem) return; - const emailEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-email')); - const msgEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-message')); - const email = emailEl ? emailEl.value.trim() : ''; - const message = msgEl ? msgEl.value.trim() : ''; - - if (!email || !isEmailValid(email)) { - ui.showNotification(i18n.t('shared_invalidEmail', 'Please enter a valid email address'), 'error'); - return; - } - - if (fileSharing?.sendShareNotification) { - fileSharing - .sendShareNotification(this.currentItem.url, email, message) - .then(() => { - this.closeNotificationDialog(); - ui.showNotification(i18n.t('shared_notificationSent', 'Notification sent'), 'success'); - }) - .catch(() => ui.showNotification(i18n.t('shared_notificationFailed', 'Failed to send notification'), 'error')); - } - } -}; - -export { sharedView };