refactor(front): remove dead code (old Shared section)

This commit is contained in:
Edouard Vanbelle
2026-05-29 12:54:52 +02:00
parent 6e30353c4b
commit ae4fb7603a
8 changed files with 9 additions and 1003 deletions
-294
View File
@@ -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;
}
}
-2
View File
@@ -13,7 +13,6 @@
<link rel="stylesheet" href="/css/views/inlineViewer.css"> <link rel="stylesheet" href="/css/views/inlineViewer.css">
<link rel="stylesheet" href="/css/views/favorites.css"> <link rel="stylesheet" href="/css/views/favorites.css">
<link rel="stylesheet" href="/css/views/recent.css"> <link rel="stylesheet" href="/css/views/recent.css">
<link rel="stylesheet" href="/css/views/shared.css">
<link rel="stylesheet" href="/css/views/sharedWithMe.css"> <link rel="stylesheet" href="/css/views/sharedWithMe.css">
<link rel="stylesheet" href="/css/views/mySharesView.css"> <link rel="stylesheet" href="/css/views/mySharesView.css">
<link rel="stylesheet" href="/css/views/trash.css"> <link rel="stylesheet" href="/css/views/trash.css">
@@ -41,7 +40,6 @@
<script defer type="module" src="/js/features/library/photos.js"></script> <script defer type="module" src="/js/features/library/photos.js"></script>
<script defer type="module" src="/js/features/library/music.js"></script> <script defer type="module" src="/js/features/library/music.js"></script>
<script defer type="module" src="/js/features/sharing/fileSharing.js"></script> <script defer type="module" src="/js/features/sharing/fileSharing.js"></script>
<script defer type="module" src="/js/views/shared/sharedView.js"></script>
<script defer type="module" src="/js/model/recentModel.js"></script> <script defer type="module" src="/js/model/recentModel.js"></script>
<script defer type="module" src="/js/views/recent/recentView.js"></script> <script defer type="module" src="/js/views/recent/recentView.js"></script>
<script defer type="module" src="/js/features/files/inlineViewer.js"></script> <script defer type="module" src="/js/features/files/inlineViewer.js"></script>
+2 -6
View File
@@ -18,7 +18,6 @@ import { recent } from '../features/library/recent.js';
import { fileSharing } from '../features/sharing/fileSharing.js'; import { fileSharing } from '../features/sharing/fileSharing.js';
import { grants } from '../model/grants.js'; import { grants } from '../model/grants.js';
import { recentView } from '../views/recent/recentView.js'; import { recentView } from '../views/recent/recentView.js';
import { sharedView } from '../views/shared/sharedView.js';
import { checkAuthentication } from './authSession.js'; import { checkAuthentication } from './authSession.js';
import { loadFiles } from './filesView.js'; import { loadFiles } from './filesView.js';
import { import {
@@ -700,11 +699,8 @@ function setupEventListeners() {
if (searchDebounceTimer) clearTimeout(searchDebounceTimer); if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
const query = elements.searchInput?.value.trim(); const query = elements.searchInput?.value.trim();
// In shared section, filter locally // My Shares section does not support in-page search
if (app.currentSection === 'shared' && sharedView) { if (app.currentSection === 'shared') return;
sharedView.filterAndSortItems();
return;
}
if (query) { if (query) {
performSearch(query); performSearch(query);
+2 -7
View File
@@ -13,7 +13,6 @@ import { photosView } from '../features/library/photos.js';
import { favoritesView } from '../views/favorites/favoritesView.js'; import { favoritesView } from '../views/favorites/favoritesView.js';
import { mySharesView } from '../views/myShares/mySharesView.js'; import { mySharesView } from '../views/myShares/mySharesView.js';
import { recentView } from '../views/recent/recentView.js'; import { recentView } from '../views/recent/recentView.js';
import { sharedView } from '../views/shared/sharedView.js';
import { sharedWithMeView } from '../views/sharedWithMe/sharedWithMeView.js'; import { sharedWithMeView } from '../views/sharedWithMe/sharedWithMeView.js';
import { filesView, loadFiles } from './filesView.js'; import { filesView, loadFiles } from './filesView.js';
import { setActionsBarMode, setGroupByView, syncGroupByMenu } from './main.js'; import { setActionsBarMode, setGroupByView, syncGroupByMenu } from './main.js';
@@ -166,9 +165,8 @@ function setCurrentSection(section) {
appElements.pageTitle.setAttribute('data-i18n', titleKey); 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 (section !== 'shared') {
if (sharedView) sharedView.hide();
mySharesView.hide(); mySharesView.hide();
} }
@@ -301,10 +299,7 @@ function switchToFilesSection() {
ui.updateBreadcrumb(); ui.updateBreadcrumb();
if (batchToolbar) batchToolbar.clear(); if (batchToolbar) batchToolbar.clear();
// temp solution loadFiles();
sharedView.loadItems().then(() => {
loadFiles();
});
} }
function switchToFavoritesSection() { function switchToFavoritesSection() {
+1 -2
View File
@@ -858,8 +858,7 @@ const shareModal = {
const expiresTs = e._draft.expires_at ? Math.floor(new Date(e._draft.expires_at).getTime() / 1000) : null; const expiresTs = e._draft.expires_at ? Math.floor(new Date(e._draft.expires_at).getTime() / 1000) : null;
await fileSharing.updateSharedLink(e.share.id, { await fileSharing.updateSharedLink(e.share.id, {
password: e._draft.password, password: e._draft.password,
expires_at: expiresTs, expires_at: expiresTs
permissions: null
}); });
} }
} }
+2 -12
View File
@@ -46,13 +46,6 @@
* @property {number} sort_date * @property {number} sort_date
*/ */
/**
* @typedef {Object} SharePermissions
* @property {boolean} read
* @property {boolean} reshare
* @property {boolean} write
*/
/** /**
* @typedef {Object} ShareItem * @typedef {Object} ShareItem
* @property {number} access_count * @property {number} access_count
@@ -64,7 +57,6 @@
* @property {string} item_id * @property {string} item_id
* @property {string} item_name * @property {string} item_name
* @property {ItemTypeEnum} item_type * @property {ItemTypeEnum} item_type
* @property {SharePermissions} permissions
* @property {string | null} token * @property {string | null} token
* @property {string} url * @property {string} url
*/ */
@@ -76,14 +68,12 @@
* @property {ItemTypeEnum} item_type * @property {ItemTypeEnum} item_type
* @property {string|null} password * @property {string|null} password
* @property {number|null} expires_at - timestamp * @property {number|null} expires_at - timestamp
* @property {SharePermissions|null} permissions
*/ */
/** /**
* @typedef {Object} UpdateShare * @typedef {Object} UpdateShare
* @property {string|null} [password] * @property {string|null} [password]
* @property {number|null} [expires_at] * @property {number|null} [expires_at]
* @property {SharePermissions|null} [permissions]
*/ */
/** /**
+2 -8
View File
@@ -35,12 +35,7 @@ const fileSharing = {
item_name: options.item_name || null, item_name: options.item_name || null,
item_type: itemType, item_type: itemType,
password: options.password || null, password: options.password || null,
expires_at: options.expires_at ? Math.floor(new Date(options.expires_at).getTime() / 1000) : 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
}
}; };
const res = await fetch('/api/shares', { const res = await fetch('/api/shares', {
@@ -113,12 +108,11 @@ const fileSharing = {
/** /**
* Update a shared link * Update a shared link
* @param {string} shareId * @param {string} shareId
* @param {UpdateShare} updateData - { permissions, password, expires_at } * @param {UpdateShare} updateData - { password, expires_at }
* @returns {Promise<Object>} Updated ShareDto * @returns {Promise<Object>} Updated ShareDto
*/ */
async updateSharedLink(shareId, updateData) { async updateSharedLink(shareId, updateData) {
const body = {}; const body = {};
if (updateData.permissions) body.permissions = updateData.permissions;
if (updateData.password !== undefined) body.password = updateData.password; if (updateData.password !== undefined) body.password = updateData.password;
if (updateData.expires_at !== undefined) body.expires_at = updateData.expires_at; if (updateData.expires_at !== undefined) body.expires_at = updateData.expires_at;
-672
View File
@@ -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<ShareItem>} */
items: [],
_expires: 0,
/** @type {Map<string, boolean>} key = "file:<id>" | "folder:<id>" */
_knownItemsId: new Map(),
/** @type {Array<ShareItem>} */
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 = `
<div class="shared-header">
<div class="shared-filters">
<div class="shared-custom-select" id="filter-type-wrapper">
<button class="shared-select-toggle" id="filter-type-toggle">
<span class="shared-select-label" data-i18n="shared_filterAll">All</span>
<i class="fas fa-chevron-down shared-select-arrow"></i>
</button>
<div class="shared-select-dropdown" id="filter-type-dropdown">
<div class="shared-select-option active" data-value="all" data-i18n="shared_filterAll">All</div>
<div class="shared-select-option" data-value="file" data-i18n="shared_filterFiles">Files</div>
<div class="shared-select-option" data-value="folder" data-i18n="shared_filterFolders">Folders</div>
</div>
</div>
<div class="shared-custom-select" id="sort-by-wrapper">
<button class="shared-select-toggle" id="sort-by-toggle">
<span class="shared-select-label" data-i18n="shared_sortByDate">Sort by date</span>
<i class="fas fa-chevron-down shared-select-arrow"></i>
</button>
<div class="shared-select-dropdown" id="sort-by-dropdown">
<div class="shared-select-option active" data-value="date" data-i18n="shared_sortByDate">Sort by date</div>
<div class="shared-select-option" data-value="name" data-i18n="shared_sortByName">Sort by name</div>
<div class="shared-select-option" data-value="expiration" data-i18n="shared_sortByExpiration">Sort by expiration</div>
</div>
</div>
</div>
</div>
<div id="empty-shared-state" class="empty-state hidden">
<i class="fas fa-share-alt empty-state-icon"></i>
<p data-i18n="shared_emptyStateTitle">No shared items</p>
<p data-i18n="shared_emptyStateDesc">Items you share will appear here</p>
<button id="go-to-files-btn" class="button primary" data-i18n="shared.goToFiles">Go to Files</button>
</div>
<div class="shared-list-container hidden">
<table class="shared-table">
<thead>
<tr>
<th data-i18n="shared_colName">Name</th>
<th data-i18n="shared_colType">Type</th>
<th data-i18n="shared_colDateShared">Date</th>
<th data-i18n="shared_colExpiration">Expiration</th>
<th data-i18n="shared_colPermissions">Permissions</th>
<th data-i18n="shared_colPassword">Password</th>
<th data-i18n="shared_colActions">Actions</th>
</tr>
</thead>
<tbody id="shared-items-list"></tbody>
</table>
</div>
<!-- Share Edit Dialog (sharedView-specific) -->
<div id="shared-view-edit-dialog" class="shared-dialog hidden">
<div class="shared-dialog-content">
<div class="shared-dialog-header">
<span id="sv-dialog-icon">📄</span>
<span id="sv-dialog-name">Item</span>
<button class="close-dialog-btn">&times;</button>
</div>
<div class="share-link-section">
<label data-i18n="share.linkLabel">Share Link:</label>
<div class="share-link-input">
<input type="text" id="sv-share-link-url" readonly>
<button id="sv-copy-link-btn" class="button" data-i18n="share.copyLink">Copy</button>
</div>
</div>
<div class="share-permissions-section">
<h4 data-i18n="share.permissions">Permissions</h4>
<label><input type="checkbox" id="sv-permission-read" checked> <span data-i18n="share.permissionRead">Read</span></label>
<label><input type="checkbox" id="sv-permission-write"> <span data-i18n="share.permissionWrite">Write</span></label>
<label><input type="checkbox" id="sv-permission-reshare"> <span data-i18n="share.permissionReshare">Reshare</span></label>
</div>
<div class="share-password-section">
<label><input type="checkbox" id="sv-enable-password"> <span data-i18n="share.password">Password protection</span></label>
<div class="password-input-group">
<input type="text" id="sv-share-password" disabled placeholder="Enter password">
<button id="sv-generate-password" class="button small" data-i18n="share.generatePassword">Generate</button>
</div>
</div>
<div class="share-expiration-section">
<label><input type="checkbox" id="sv-enable-expiration"> <span data-i18n="share.expiration">Set expiration</span></label>
<input type="date" id="sv-share-expiration" disabled>
</div>
<div class="share-actions">
<button id="sv-update-share-btn" class="button primary" data-i18n="share.update">Update</button>
<button id="sv-remove-share-btn" class="button danger" data-i18n="share.remove">Remove Share</button>
</div>
</div>
</div>
<!-- Notification Dialog (sharedView-specific) -->
<div id="sv-notification-dialog" class="shared-dialog hidden">
<div class="shared-dialog-content">
<div class="shared-dialog-header">
<span id="sv-notify-dialog-icon">📧</span>
<span id="sv-notify-dialog-name">Item</span>
<button class="close-dialog-btn">&times;</button>
</div>
<div class="notification-form">
<div class="form-group">
<label data-i18n="share.notifyEmailLabel">Email:</label>
<input type="email" id="sv-notification-email" placeholder="recipient@example.com">
</div>
<div class="form-group">
<label data-i18n="share.notifyMessageLabel">Message (optional):</label>
<textarea id="sv-notification-message" rows="3"></textarea>
</div>
</div>
<div class="notification-actions">
<button id="sv-send-notification-btn" class="button primary" data-i18n="share.notifySend">Send Notification</button>
</div>
</div>
</div>
`;
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 = '<span class="action-icon">✏️</span>';
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 = '<span class="action-icon">📧</span>';
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 = '<span class="action-icon">📋</span>';
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 = '<span class="action-icon">🗑️</span>';
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 };