style(ui): request that all types defined
- check in more restrictive mode = request types - define main types in static/js/core/types.js
This commit is contained in:
@@ -405,7 +405,7 @@ function initLanguageSelector() {
|
||||
const pickerName = document.getElementById('lang-picker-name');
|
||||
const searchInput = document.getElementById('lang-picker-search-input');
|
||||
|
||||
if (!languagePanel || !picker) return;
|
||||
if (!languagePanel || !picker || !pickerFlag || !pickerName || !pickerList) return;
|
||||
|
||||
// --- Auto-detect browser language ---
|
||||
const detected = detectBrowserLanguage();
|
||||
|
||||
@@ -271,7 +271,7 @@ const fileOps = {
|
||||
|
||||
/**
|
||||
* Upload files to the server with real-time progress indication
|
||||
* @param {FileList} files - Files to upload
|
||||
* @param {FileList | File[]} files - Files to upload
|
||||
*/
|
||||
async uploadFiles(files) {
|
||||
const originalFiles = Array.from(files || []);
|
||||
|
||||
@@ -146,14 +146,14 @@ const search = {
|
||||
if (previousSearchHeader) {
|
||||
previousSearchHeader.replaceWith(searchHeader);
|
||||
} else {
|
||||
pageStickyHeader.appendChild(searchHeader);
|
||||
pageStickyHeader?.appendChild(searchHeader);
|
||||
}
|
||||
|
||||
// Sort dropdown — re-searches with new sort order (server-side)
|
||||
const sortSelect = document.getElementById('search-sort-select');
|
||||
const sortSelect = /** @type {HTMLSelectElement} */ (document.getElementById('search-sort-select'));
|
||||
if (sortSelect) {
|
||||
sortSelect.addEventListener('change', () => {
|
||||
const searchInput = document.querySelector('.search-container input');
|
||||
const searchInput = /** @type {HTMLInputElement} */ (document.querySelector('.search-container input'));
|
||||
if (searchInput?.value.trim()) {
|
||||
const event = new CustomEvent('search-resort', {
|
||||
detail: { sort_by: sortSelect.value }
|
||||
@@ -167,11 +167,12 @@ const search = {
|
||||
const clearSearchBtn = document.getElementById('clear-search-btn');
|
||||
if (clearSearchBtn) {
|
||||
clearSearchBtn.addEventListener('click', () => {
|
||||
document.querySelector('.search-container input').value = '';
|
||||
const searchInput = /** @type {HTMLInputElement} */ (document.querySelector('.search-container input'));
|
||||
searchInput.value = '';
|
||||
app.currentPath = '';
|
||||
app.isSearchMode = false;
|
||||
document.querySelector('.search-results-header')?.remove();
|
||||
ui.updateBreadcrumb('');
|
||||
ui.updateBreadcrumb();
|
||||
loadFiles();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { loadFiles } from '../../app/filesView.js';
|
||||
import { ui } from '../../app/ui.js';
|
||||
|
||||
class WopiEditor {
|
||||
constructor() {
|
||||
@@ -46,9 +47,7 @@ class WopiEditor {
|
||||
window.open(hostUrl, '_blank');
|
||||
} catch (error) {
|
||||
console.error('Failed to open WOPI editor in tab:', error);
|
||||
if (window.showNotification) {
|
||||
window.showNotification('Could not open the document editor.', 'error');
|
||||
}
|
||||
ui.showNotification('Could not open the document editor.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ const favorites = {
|
||||
// FIXME: this case is not easy to understand, should apply better implementation
|
||||
multiSelect.init();
|
||||
|
||||
ui.updateBreadcrumb('');
|
||||
ui.updateBreadcrumb();
|
||||
|
||||
if (this._cache.size === 0) {
|
||||
ui.showError(`
|
||||
|
||||
@@ -214,7 +214,7 @@ const photosView = {
|
||||
if (grid?.classList.contains('photos-grid')) {
|
||||
grid.insertAdjacentHTML('beforeend', tilesHtml);
|
||||
const countSpan = existingHeader.querySelector('.photos-day-count');
|
||||
if (countSpan) countSpan.textContent = grid.children.length;
|
||||
if (countSpan) countSpan.textContent = String(grid.children.length);
|
||||
}
|
||||
} else {
|
||||
// New group — insert header + grid before sentinel
|
||||
@@ -269,11 +269,14 @@ const photosView = {
|
||||
/** @param {number} [startIndex=0] When > 0, only process video tiles
|
||||
* for items[startIndex..] — avoids re-scanning the entire DOM. */
|
||||
_setupVideoThumbnails(startIndex = 0) {
|
||||
const tiles = this._container.querySelectorAll('.photo-tile[data-mime^="video/"]');
|
||||
const tiles = /** @type {NodeListOf<HTMLDivElement> */ (this._container?.querySelectorAll('.photo-tile[data-mime^="video/"]'));
|
||||
const newIds = startIndex > 0 ? new Set(this.items.slice(startIndex).map((f) => f.id)) : null;
|
||||
|
||||
if (!tiles) return;
|
||||
|
||||
for (const tile of tiles) {
|
||||
const fileId = tile.dataset.id;
|
||||
if (!fileId) continue;
|
||||
if (newIds && !newIds.has(fileId)) continue;
|
||||
if (this._videoThumbCache.has(fileId)) continue;
|
||||
|
||||
@@ -313,6 +316,8 @@ const photosView = {
|
||||
/** Extract a single frame from a video and display it as the tile
|
||||
* thumbnail, then upload the JPEG to the server for caching. */
|
||||
_generateVideoThumbnail(tile, img) {
|
||||
// TODO: use thumbnail.js s common lib
|
||||
|
||||
const fileId = tile.dataset.id;
|
||||
const video = document.createElement('video');
|
||||
video.crossOrigin = 'anonymous';
|
||||
@@ -341,7 +346,7 @@ const photosView = {
|
||||
canvas.width = Math.round(video.videoWidth * scale);
|
||||
canvas.height = Math.round(video.videoHeight * scale);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
ctx?.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// JPEG: explicit quality control, universally supported,
|
||||
// and server stores as-is when dimensions fit (zero re-encode).
|
||||
@@ -362,7 +367,7 @@ const photosView = {
|
||||
|
||||
// Upload to server for permanent caching
|
||||
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
||||
const headers = { 'Content-Type': blob.type, ...getCsrfHeaders() };
|
||||
const headers = /** @type {Record<String, String>} */ ({ 'Content-Type': blob.type, ...getCsrfHeaders() });
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
fetch(`/api/files/${fileId}/thumbnail/preview`, {
|
||||
@@ -426,6 +431,7 @@ const photosView = {
|
||||
|
||||
/** Render empty state */
|
||||
_renderEmpty() {
|
||||
if (!this._container) return;
|
||||
this._container.innerHTML = `
|
||||
<div class="photos-empty">
|
||||
<i class="fas fa-images"></i>
|
||||
@@ -526,44 +532,53 @@ const photosView = {
|
||||
<button id="photos-sel-clear" title="Clear"><i class="fas fa-times"></i></button>
|
||||
`;
|
||||
|
||||
bar.querySelector('#photos-sel-clear').onclick = () => {
|
||||
this.selected.clear();
|
||||
this._container.querySelectorAll('.photo-tile.selected').forEach((t) => {
|
||||
t.classList.remove('selected');
|
||||
});
|
||||
this._hideSelectionBar();
|
||||
};
|
||||
const bar_clear = /** @type {HTMLButtonElement} */ (bar.querySelector('#photos-sel-clear'));
|
||||
if (bar_clear) {
|
||||
bar_clear.onclick = () => {
|
||||
this.selected.clear();
|
||||
this._container.querySelectorAll('.photo-tile.selected').forEach((t) => {
|
||||
t.classList.remove('selected');
|
||||
});
|
||||
this._hideSelectionBar();
|
||||
};
|
||||
}
|
||||
|
||||
bar.querySelector('#photos-sel-delete').onclick = async () => {
|
||||
if (!confirm('Delete selected items?')) return;
|
||||
for (const fid of this.selected) {
|
||||
try {
|
||||
await fetch(`/api/files/${fid}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
headers: this._headers()
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Delete failed:', fid, err);
|
||||
const bar_delete = /** @type {HTMLButtonElement} */ (bar.querySelector('#photos-sel-delete'));
|
||||
if (bar_delete) {
|
||||
bar_delete.onclick = async () => {
|
||||
if (!confirm('Delete selected items?')) return;
|
||||
for (const fid of this.selected) {
|
||||
try {
|
||||
await fetch(`/api/files/${fid}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
headers: this._headers()
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Delete failed:', fid, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.items = this.items.filter((f) => !this.selected.has(f.id));
|
||||
this.selected.clear();
|
||||
this._hideSelectionBar();
|
||||
this._renderedCount = 0;
|
||||
this._renderFull();
|
||||
};
|
||||
this.items = this.items.filter((f) => !this.selected.has(f.id));
|
||||
this.selected.clear();
|
||||
this._hideSelectionBar();
|
||||
this._renderedCount = 0;
|
||||
this._renderFull();
|
||||
};
|
||||
}
|
||||
|
||||
bar.querySelector('#photos-sel-download').onclick = async () => {
|
||||
for (const fid of this.selected) {
|
||||
const a = document.createElement('a');
|
||||
a.href = `/api/files/${fid}`;
|
||||
a.download = '';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
};
|
||||
const bar_download = /** @type {HTMLButtonElement} */ (bar.querySelector('#photos-sel-download'));
|
||||
if (bar_download) {
|
||||
bar_download.onclick = async () => {
|
||||
for (const fid of this.selected) {
|
||||
const a = document.createElement('a');
|
||||
a.href = `/api/files/${fid}`;
|
||||
a.download = '';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bar.style.display = 'flex';
|
||||
},
|
||||
|
||||
@@ -109,7 +109,7 @@ const recent = {
|
||||
multiSelect.clear();
|
||||
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
||||
}
|
||||
ui.updateBreadcrumb('');
|
||||
ui.updateBreadcrumb();
|
||||
|
||||
if (recentItems.length === 0) {
|
||||
ui.showError(`
|
||||
|
||||
@@ -165,7 +165,7 @@ const fileSharing = {
|
||||
|
||||
/**
|
||||
* Format expiration date for display (Unix timestamp in seconds or ISO string)
|
||||
* @param {number|string} value
|
||||
* @param {number|Date} value
|
||||
* @returns {string}
|
||||
*/
|
||||
formatExpirationDate(value) {
|
||||
@@ -177,7 +177,7 @@ const fileSharing = {
|
||||
* Send a notification about a shared resource (stub — no backend endpoint yet)
|
||||
* @param {string} shareUrl
|
||||
* @param {string} recipientEmail
|
||||
* @param {string} message
|
||||
* @param {string} _message
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async sendShareNotification(shareUrl, recipientEmail, _message = '') {
|
||||
|
||||
Reference in New Issue
Block a user