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:
+10
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"includes": ["static/**/*.js", "static/**/*.css", "static/**/*.json"]
|
"includes": ["static/**/*.js", "static/**/*.css", "static/**/*.json", "!static/js/vendors/"]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -15,7 +15,15 @@
|
|||||||
"recommended": true,
|
"recommended": true,
|
||||||
"correctness": {
|
"correctness": {
|
||||||
"noUnusedVariables": "warn",
|
"noUnusedVariables": "warn",
|
||||||
"noUndeclaredVariables": "error"
|
"noUndeclaredVariables": "error",
|
||||||
|
"noUnreachable": "warn",
|
||||||
|
"noUnsafeFinally": "error"
|
||||||
|
},
|
||||||
|
"nursery": {
|
||||||
|
"useExplicitType": "error"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"noGlobalEval": "error"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"noDescendingSpecificity": "off"
|
"noDescendingSpecificity": "off"
|
||||||
|
|||||||
+9
-4
@@ -2,17 +2,22 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Enable type checking on all JS files (equivalent to @ts-check globally)
|
// Enable type checking on all JS files (equivalent to @ts-check globally)
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": true,
|
"noEmit": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
"target": "ES2022",
|
|
||||||
"lib": ["ES2022", "DOM"],
|
"lib": ["ES2022", "DOM"],
|
||||||
// Treat all JS files as modules
|
// Treat all JS files as modules
|
||||||
"moduleDetection": "force"
|
"moduleDetection": "force",
|
||||||
|
"strictNullChecks": false, // too much pedantic...
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"target": "ESNext"
|
||||||
},
|
},
|
||||||
"include": ["static/js/**/*.js"],
|
"include": ["static/js/**/*.js"],
|
||||||
"exclude": []
|
"exclude": ["static/js/vendors/**", "static/js/vendors/**/*.mjs"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,23 +9,9 @@ import { app } from './state.js';
|
|||||||
import { ui } from './ui.js';
|
import { ui } from './ui.js';
|
||||||
import { uiNotifications } from './uiNotifications.js';
|
import { uiNotifications } from './uiNotifications.js';
|
||||||
|
|
||||||
let isLoadingFiles = false;
|
/** @import {FileInfo, FolderInfo} from '../core/types.js' */
|
||||||
|
|
||||||
// TODO move to features/files/fileOperations.js ?
|
let isLoadingFiles = false;
|
||||||
/**
|
|
||||||
* @typedef {Object} FolderInfo
|
|
||||||
* @property {string} category
|
|
||||||
* @property {number} created_at - timestamp
|
|
||||||
* @property {string} icon_class
|
|
||||||
* @property {string} icon_special_class
|
|
||||||
* @property {string} id the uniq id of the folder
|
|
||||||
* @property {boolean} is_root
|
|
||||||
* @property {number} modified_at
|
|
||||||
* @property {string} name
|
|
||||||
* @property {string} owner_id
|
|
||||||
* @property {string|null} parent_id the folder parent (null if is_root)
|
|
||||||
* @property {string} path the full path
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getFolder information
|
* getFolder information
|
||||||
@@ -99,7 +85,7 @@ async function rebuildBreadCrumb() {
|
|||||||
uiNotifications.show('error: folder not found or permission denied', 'the given folder is not available or you do not have sufficient rights');
|
uiNotifications.show('error: folder not found or permission denied', 'the given folder is not available or you do not have sufficient rights');
|
||||||
app.breadcrumbPath = [];
|
app.breadcrumbPath = [];
|
||||||
id = app.userHomeFolderId;
|
id = app.userHomeFolderId;
|
||||||
app.currentPath = id;
|
if (id) app.currentPath = id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +100,7 @@ async function rebuildBreadCrumb() {
|
|||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {boolean} [options.insertHistory] add browser history (default true)
|
* @param {boolean} [options.insertHistory] add browser history (default true)
|
||||||
* @param {boolean} [options.forceRefresh] force refresh of content
|
* @param {boolean} [options.forceRefresh] force refresh of content
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
async function loadFiles(options = { insertHistory: true }) {
|
async function loadFiles(options = { insertHistory: true }) {
|
||||||
try {
|
try {
|
||||||
@@ -185,7 +172,7 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
|
|
||||||
if (forceRefresh) {
|
if (forceRefresh) {
|
||||||
url += `&force_refresh=true`;
|
url += `&force_refresh=true`;
|
||||||
requestOptions.headers['X-Force-Refresh'] = 'true';
|
if (requestOptions.headers) requestOptions.headers['X-Force-Refresh'] = 'true';
|
||||||
console.log('Forcing complete refresh ignoring cache');
|
console.log('Forcing complete refresh ignoring cache');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +202,10 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {FolderInfo[]} */
|
||||||
const folderList = Array.isArray(listing.folders) ? listing.folders : [];
|
const folderList = Array.isArray(listing.folders) ? listing.folders : [];
|
||||||
|
|
||||||
|
/** @type {FileInfo[]} */
|
||||||
const fileList = Array.isArray(listing.files) ? listing.files : [];
|
const fileList = Array.isArray(listing.files) ? listing.files : [];
|
||||||
|
|
||||||
if (folderList.length === 0 && fileList.length === 0) {
|
if (folderList.length === 0 && fileList.length === 0) {
|
||||||
|
|||||||
+35
-27
@@ -31,7 +31,7 @@ import { ui } from './ui.js';
|
|||||||
import { setupUserMenu } from './userMenu.js';
|
import { setupUserMenu } from './userMenu.js';
|
||||||
|
|
||||||
// Upload dropdown listener state (prevents accumulated listeners)
|
// Upload dropdown listener state (prevents accumulated listeners)
|
||||||
/** @type { function | null } */
|
/** @type {((e: MouseEvent) => void) | null} */
|
||||||
let uploadDropdownDocumentClickHandler = null;
|
let uploadDropdownDocumentClickHandler = null;
|
||||||
|
|
||||||
/** @type { AbortController | null } */
|
/** @type { AbortController | null } */
|
||||||
@@ -178,7 +178,7 @@ function setupActionsBarDelegation() {
|
|||||||
actionsBarDelegationBound = true;
|
actionsBarDelegationBound = true;
|
||||||
|
|
||||||
elements.actionsBar.addEventListener('click', async (e) => {
|
elements.actionsBar.addEventListener('click', async (e) => {
|
||||||
const btn = e.target.closest('button');
|
const btn = /** @type {HTMLElement} */ (e.target)?.closest('button');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
|
|
||||||
switch (btn.id) {
|
switch (btn.id) {
|
||||||
@@ -321,7 +321,7 @@ function switchSectionTo(section) {
|
|||||||
// no change ...
|
// no change ...
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ((!section) in SECTIONS_MAPPER) {
|
if (!(section in SECTIONS_MAPPER)) {
|
||||||
console.warn(`context view ${section} unkonwn fallback to files section`);
|
console.warn(`context view ${section} unkonwn fallback to files section`);
|
||||||
section = 'files';
|
section = 'files';
|
||||||
}
|
}
|
||||||
@@ -420,7 +420,7 @@ function initApp() {
|
|||||||
function cacheElements() {
|
function cacheElements() {
|
||||||
elements.uploadBtn = document.getElementById('upload-btn');
|
elements.uploadBtn = document.getElementById('upload-btn');
|
||||||
elements.dropzone = document.getElementById('dropzone');
|
elements.dropzone = document.getElementById('dropzone');
|
||||||
elements.fileInput = document.getElementById('file-input');
|
elements.fileInput = /** @type {HTMLInputElement} */ (document.getElementById('file-input'));
|
||||||
elements.filesList = document.getElementById('files-list');
|
elements.filesList = document.getElementById('files-list');
|
||||||
elements.newFolderBtn = document.getElementById('new-folder-btn');
|
elements.newFolderBtn = document.getElementById('new-folder-btn');
|
||||||
elements.gridViewBtn = document.getElementById('grid-view-btn');
|
elements.gridViewBtn = document.getElementById('grid-view-btn');
|
||||||
@@ -472,7 +472,7 @@ function setupUploadDropdown() {
|
|||||||
document.removeEventListener('click', uploadDropdownDocumentClickHandler);
|
document.removeEventListener('click', uploadDropdownDocumentClickHandler);
|
||||||
}
|
}
|
||||||
uploadDropdownDocumentClickHandler = (e) => {
|
uploadDropdownDocumentClickHandler = (e) => {
|
||||||
if (e.target.closest('#upload-dropdown')) return;
|
if (/** @type {HTMLElement} */ (e.target)?.closest('#upload-dropdown')) return;
|
||||||
document.querySelectorAll('.upload-dropdown-menu').forEach((m) => {
|
document.querySelectorAll('.upload-dropdown-menu').forEach((m) => {
|
||||||
m.classList.add('hidden');
|
m.classList.add('hidden');
|
||||||
});
|
});
|
||||||
@@ -511,11 +511,11 @@ function setupEventListeners() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Search input — Enter key
|
// Search input — Enter key
|
||||||
elements.searchInput.addEventListener('keydown', (e) => {
|
elements.searchInput?.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
// Cancel any pending debounce
|
// Cancel any pending debounce
|
||||||
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
|
// In shared section, filter locally
|
||||||
if (app.currentSection === 'shared' && sharedView) {
|
if (app.currentSection === 'shared' && sharedView) {
|
||||||
@@ -529,16 +529,17 @@ function setupEventListeners() {
|
|||||||
// If search is empty and we're in search mode, return to normal view
|
// If search is empty and we're in search mode, return to normal view
|
||||||
app.isSearchMode = false;
|
app.isSearchMode = false;
|
||||||
app.currentPath = '';
|
app.currentPath = '';
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search input — Live search (debounced, after 3+ chars)
|
// Search input — Live search (debounced, after 3+ chars)
|
||||||
elements.searchInput.addEventListener('input', () => {
|
elements.searchInput?.addEventListener('input', () => {
|
||||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
||||||
const query = elements.searchInput.value.trim();
|
const query = elements.searchInput?.value.trim();
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
if (query.length >= SEARCH_MIN_CHARS) {
|
if (query.length >= SEARCH_MIN_CHARS) {
|
||||||
searchDebounceTimer = setTimeout(() => {
|
searchDebounceTimer = setTimeout(() => {
|
||||||
@@ -549,16 +550,16 @@ function setupEventListeners() {
|
|||||||
searchDebounceTimer = setTimeout(() => {
|
searchDebounceTimer = setTimeout(() => {
|
||||||
app.isSearchMode = false;
|
app.isSearchMode = false;
|
||||||
app.currentPath = '';
|
app.currentPath = '';
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
}, SEARCH_DEBOUNCE_MS);
|
}, SEARCH_DEBOUNCE_MS);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search button
|
// Search button
|
||||||
document.getElementById('search-button').addEventListener('click', () => {
|
document.getElementById('search-button')?.addEventListener('click', () => {
|
||||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
||||||
const query = elements.searchInput.value.trim();
|
const query = elements.searchInput?.value.trim();
|
||||||
if (query) {
|
if (query) {
|
||||||
performSearch(query);
|
performSearch(query);
|
||||||
}
|
}
|
||||||
@@ -572,10 +573,13 @@ function setupEventListeners() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// File input
|
// File input
|
||||||
elements.fileInput.addEventListener('change', (e) => {
|
elements.fileInput?.addEventListener('change', (e) => {
|
||||||
if (e.target.files.length > 0) {
|
const target = /** @type {HTMLInputElement} */ (e.target);
|
||||||
fileOps.uploadFiles(e.target.files);
|
if (!target) return;
|
||||||
e.target.value = ''; // reset so same file can be re-uploaded
|
if (!target.files) return;
|
||||||
|
if (target.files.length > 0) {
|
||||||
|
fileOps.uploadFiles(target.files);
|
||||||
|
target.value = ''; // reset so same file can be re-uploaded
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -583,18 +587,21 @@ function setupEventListeners() {
|
|||||||
const folderInput = document.getElementById('folder-input');
|
const folderInput = document.getElementById('folder-input');
|
||||||
if (folderInput) {
|
if (folderInput) {
|
||||||
folderInput.addEventListener('change', (e) => {
|
folderInput.addEventListener('change', (e) => {
|
||||||
if (e.target.files.length > 0) {
|
const target = /** @type {HTMLInputElement} */ (e.target);
|
||||||
fileOps.uploadFolderFiles(e.target.files);
|
if (!target) return;
|
||||||
e.target.value = '';
|
if (!target.files) return;
|
||||||
|
if (target.files.length > 0) {
|
||||||
|
fileOps.uploadFolderFiles(target.files);
|
||||||
|
target.value = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sidebar navigation
|
// Sidebar navigation
|
||||||
elements.navItems.forEach((item) => {
|
elements.navItems?.forEach((item) => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
// Remove active class from all nav items
|
// Remove active class from all nav items
|
||||||
elements.navItems.forEach((navItem) => {
|
elements.navItems?.forEach((navItem) => {
|
||||||
navItem.classList.remove('active');
|
navItem.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -602,7 +609,7 @@ function setupEventListeners() {
|
|||||||
item.classList.add('active');
|
item.classList.add('active');
|
||||||
let _updateHistory = true;
|
let _updateHistory = true;
|
||||||
|
|
||||||
const itemI18nKey = item.querySelector('span').getAttribute('data-i18n');
|
const itemI18nKey = item.querySelector('span')?.getAttribute('data-i18n');
|
||||||
|
|
||||||
switch (itemI18nKey) {
|
switch (itemI18nKey) {
|
||||||
case 'nav.shared':
|
case 'nav.shared':
|
||||||
@@ -661,12 +668,13 @@ function setupEventListeners() {
|
|||||||
// Global events to close context menus and deselect cards
|
// Global events to close context menus and deselect cards
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const folderMenu = document.getElementById('folder-context-menu');
|
const folderMenu = document.getElementById('folder-context-menu');
|
||||||
if (folderMenu && !folderMenu.classList.contains('hidden') && !folderMenu.contains(e.target)) {
|
const target = /** @type {HTMLElement} */ (e.target);
|
||||||
|
if (folderMenu && !folderMenu.classList.contains('hidden') && !folderMenu.contains(target)) {
|
||||||
ui.closeContextMenu();
|
ui.closeContextMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileMenu = document.getElementById('file-context-menu');
|
const fileMenu = document.getElementById('file-context-menu');
|
||||||
if (fileMenu && !fileMenu.classList.contains('hidden') && !fileMenu.contains(e.target)) {
|
if (fileMenu && !fileMenu.classList.contains('hidden') && !fileMenu.contains(target)) {
|
||||||
ui.closeFileContextMenu();
|
ui.closeFileContextMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -714,8 +722,8 @@ function updateStorageUsageDisplay(userData) {
|
|||||||
const quotaFormatted = formatQuotaSize(quotaBytes);
|
const quotaFormatted = formatQuotaSize(quotaBytes);
|
||||||
|
|
||||||
// Update the storage display elements
|
// Update the storage display elements
|
||||||
const storageFill = document.querySelector('.storage-fill');
|
const storageFill = /** @type {HTMLDivElement} */ (document.querySelector('.storage-fill'));
|
||||||
const storageInfo = document.querySelector('.storage-info');
|
const storageInfo = /** @type {HTMLDivElement} */ (document.querySelector('.storage-info'));
|
||||||
|
|
||||||
if (storageFill) {
|
if (storageFill) {
|
||||||
storageFill.style.width = `${usagePercentage}%`;
|
storageFill.style.width = `${usagePercentage}%`;
|
||||||
|
|||||||
+14
-11
@@ -27,14 +27,14 @@ function syncViewContainers() {
|
|||||||
|
|
||||||
const isGrid = app.currentView === 'grid';
|
const isGrid = app.currentView === 'grid';
|
||||||
if (isGrid) {
|
if (isGrid) {
|
||||||
filesList.classList.remove('files-list-view');
|
filesList?.classList.remove('files-list-view');
|
||||||
filesList.classList.add('files-grid-view');
|
filesList?.classList.add('files-grid-view');
|
||||||
|
|
||||||
gridViewBtn?.classList.add('active');
|
gridViewBtn?.classList.add('active');
|
||||||
listViewBtn?.classList.remove('active');
|
listViewBtn?.classList.remove('active');
|
||||||
} else {
|
} else {
|
||||||
filesList.classList.add('files-list-view');
|
filesList?.classList.add('files-list-view');
|
||||||
filesList.classList.remove('files-grid-view');
|
filesList?.classList.remove('files-grid-view');
|
||||||
|
|
||||||
gridViewBtn?.classList.remove('active');
|
gridViewBtn?.classList.remove('active');
|
||||||
listViewBtn?.classList.add('active');
|
listViewBtn?.classList.add('active');
|
||||||
@@ -47,7 +47,7 @@ function syncViewContainers() {
|
|||||||
*/
|
*/
|
||||||
function toggleFileContainer(show) {
|
function toggleFileContainer(show) {
|
||||||
const filesList = document.getElementById('files-list');
|
const filesList = document.getElementById('files-list');
|
||||||
filesList.classList.toggle('hidden', !show);
|
filesList?.classList.toggle('hidden', !show);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,19 +61,19 @@ function initSidebarToggle() {
|
|||||||
if (!sidebarToggle || !sidebar || !sidebarOverlay) return;
|
if (!sidebarToggle || !sidebar || !sidebarOverlay) return;
|
||||||
|
|
||||||
function openSidebar() {
|
function openSidebar() {
|
||||||
sidebar.classList.add('open');
|
sidebar?.classList.add('open');
|
||||||
sidebarOverlay.classList.add('active');
|
sidebarOverlay?.classList.add('active');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSidebar() {
|
function closeSidebar() {
|
||||||
sidebar.classList.remove('open');
|
sidebar?.classList.remove('open');
|
||||||
sidebarOverlay.classList.remove('active');
|
sidebarOverlay?.classList.remove('active');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
if (sidebar.classList.contains('open')) {
|
if (sidebar?.classList.contains('open')) {
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
} else {
|
} else {
|
||||||
openSidebar();
|
openSidebar();
|
||||||
@@ -118,6 +118,7 @@ function getSectionFromNavItem(navItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mapping section name to associated switch functions
|
// Mapping section name to associated switch functions
|
||||||
|
/** @type {Record<String, Function>} */
|
||||||
export const SECTIONS_MAPPER = {
|
export const SECTIONS_MAPPER = {
|
||||||
files: switchToFilesSection,
|
files: switchToFilesSection,
|
||||||
shared: switchToSharedSection,
|
shared: switchToSharedSection,
|
||||||
@@ -144,7 +145,7 @@ function setCurrentSection(section) {
|
|||||||
app.currentSection = section;
|
app.currentSection = section;
|
||||||
|
|
||||||
// Update nav item active classes by finding matching item from DOM
|
// Update nav item active classes by finding matching item from DOM
|
||||||
appElements.navItems.forEach((item) => {
|
appElements.navItems?.forEach((item) => {
|
||||||
const itemSection = getSectionFromNavItem(item);
|
const itemSection = getSectionFromNavItem(item);
|
||||||
item.classList.toggle('active', itemSection === section);
|
item.classList.toggle('active', itemSection === section);
|
||||||
});
|
});
|
||||||
@@ -152,8 +153,10 @@ function setCurrentSection(section) {
|
|||||||
// Update page title
|
// Update page title
|
||||||
const titleKey = `nav.${section}`;
|
const titleKey = `nav.${section}`;
|
||||||
// TODO check why no more used: const defaultTitle = section.charAt(0).toUpperCase() + section.slice(1);
|
// TODO check why no more used: const defaultTitle = section.charAt(0).toUpperCase() + section.slice(1);
|
||||||
|
if (appElements.pageTitle) {
|
||||||
appElements.pageTitle.textContent = i18n.t(titleKey);
|
appElements.pageTitle.textContent = i18n.t(titleKey);
|
||||||
appElements.pageTitle.setAttribute('data-i18n', titleKey);
|
appElements.pageTitle.setAttribute('data-i18n', titleKey);
|
||||||
|
}
|
||||||
|
|
||||||
// Hide sharedView when switching to any other section
|
// Hide sharedView when switching to any other section
|
||||||
if (section !== 'shared' && sharedView) {
|
if (section !== 'shared' && sharedView) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ async function performSearch(query, sortBy) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
app.isSearchMode = true;
|
app.isSearchMode = true;
|
||||||
ui.updateBreadcrumb(`Search: "${query}"`);
|
ui.updateBreadcrumb();
|
||||||
|
|
||||||
ui.showError(`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`);
|
ui.showError(`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`);
|
||||||
|
|
||||||
@@ -48,9 +48,10 @@ async function performSearch(query, sortBy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('search-resort', (e) => {
|
document.addEventListener('search-resort', (e) => {
|
||||||
const searchInput = document.querySelector('.search-container input');
|
const event = /** @type {CustomEvent<{sort_by: string}>} */ (e);
|
||||||
|
const searchInput = /** @type {HTMLInputElement} */ (document.querySelector('.search-container input'));
|
||||||
if (searchInput?.value.trim()) {
|
if (searchInput?.value.trim()) {
|
||||||
performSearch(searchInput.value.trim(), e.detail.sort_by);
|
performSearch(searchInput.value.trim(), event.detail.sort_by);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+41
-1
@@ -3,15 +3,27 @@
|
|||||||
* Centralized mutable state for app and cached DOM references.
|
* Centralized mutable state for app and cached DOM references.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** @import {FolderInfo} from '../core/types.js' */
|
||||||
|
|
||||||
export const app = {
|
export const app = {
|
||||||
currentView: 'grid',
|
currentView: 'grid',
|
||||||
|
|
||||||
|
/** @type {string | null} */
|
||||||
currentPath: '',
|
currentPath: '',
|
||||||
currentFolder: null,
|
currentFolder: null,
|
||||||
|
|
||||||
|
/** @type {FolderInfo | null} */
|
||||||
currentFolderInfo: null,
|
currentFolderInfo: null,
|
||||||
|
|
||||||
|
/** @type {Object | null} */
|
||||||
contextMenuTargetFolder: null,
|
contextMenuTargetFolder: null,
|
||||||
|
|
||||||
|
/** @type {Object | null} */
|
||||||
contextMenuTargetFile: null,
|
contextMenuTargetFile: null,
|
||||||
selectedTargetFolderId: '',
|
selectedTargetFolderId: '',
|
||||||
moveDialogMode: 'file',
|
moveDialogMode: 'file',
|
||||||
|
|
||||||
|
/** @type {String | null} */
|
||||||
currentSection: null, // will be defined on first call
|
currentSection: null, // will be defined on first call
|
||||||
isSearchMode: false,
|
isSearchMode: false,
|
||||||
shareDialogItem: null,
|
shareDialogItem: null,
|
||||||
@@ -19,8 +31,36 @@ export const app = {
|
|||||||
notificationShareUrl: null,
|
notificationShareUrl: null,
|
||||||
userHomeFolderId: null,
|
userHomeFolderId: null,
|
||||||
userHomeFolderName: null,
|
userHomeFolderName: null,
|
||||||
|
/** @type {Object[]} */
|
||||||
breadcrumbPath: [], // Array of {id, name} tracking folder navigation hierarchy
|
breadcrumbPath: [], // Array of {id, name} tracking folder navigation hierarchy
|
||||||
|
|
||||||
|
/** @type {String | null} */
|
||||||
viewFile: null // current file in inline view
|
viewFile: null // current file in inline view
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appElements = {};
|
export const appElements = {
|
||||||
|
/** @type {HTMLElement | null} */
|
||||||
|
uploadBtn: null,
|
||||||
|
/** @type {HTMLElement | null} */
|
||||||
|
dropzone: null,
|
||||||
|
/** @type {HTMLInputElement | null} */
|
||||||
|
fileInput: null,
|
||||||
|
/** @type {HTMLElement | null} */
|
||||||
|
filesList: null,
|
||||||
|
/** @type {HTMLElement | null} */
|
||||||
|
newFolderBtn: null,
|
||||||
|
/** @type {HTMLElement | null} */
|
||||||
|
gridViewBtn: null,
|
||||||
|
/** @type {HTMLElement | null} */
|
||||||
|
listViewBtn: null,
|
||||||
|
/** @type {HTMLElement | null} */
|
||||||
|
breadcrumb: null,
|
||||||
|
/** @type {HTMLElement | null} */
|
||||||
|
pageTitle: null,
|
||||||
|
/** @type {HTMLElement | null} */
|
||||||
|
actionsBar: null,
|
||||||
|
/** @type {NodeListOf<HTMLElement> | null} */
|
||||||
|
navItems: null,
|
||||||
|
/** @type {HTMLInputElement | null} */
|
||||||
|
searchInput: null
|
||||||
|
};
|
||||||
|
|||||||
+65
-46
@@ -24,8 +24,6 @@ import { app } from './state.js';
|
|||||||
import { uiFileTypes } from './uiFileTypes.js';
|
import { uiFileTypes } from './uiFileTypes.js';
|
||||||
import { uiNotifications } from './uiNotifications.js';
|
import { uiNotifications } from './uiNotifications.js';
|
||||||
|
|
||||||
let __rubberBandJustFinished = false;
|
|
||||||
|
|
||||||
// UI Module
|
// UI Module
|
||||||
const ui = {
|
const ui = {
|
||||||
/** @type {HTMLDListElement | null} */
|
/** @type {HTMLDListElement | null} */
|
||||||
@@ -235,22 +233,22 @@ const ui = {
|
|||||||
document.body.appendChild(shareDialog);
|
document.body.appendChild(shareDialog);
|
||||||
|
|
||||||
// Add event listeners for share dialog
|
// Add event listeners for share dialog
|
||||||
document.getElementById('share-close-btn').addEventListener('click', () => {
|
document.getElementById('share-close-btn')?.addEventListener('click', () => {
|
||||||
contextMenus.closeShareDialog();
|
contextMenus.closeShareDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('share-confirm-btn').addEventListener('click', async () => {
|
document.getElementById('share-confirm-btn')?.addEventListener('click', async () => {
|
||||||
await contextMenus.createSharedLink();
|
await contextMenus.createSharedLink();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('copy-share-btn').addEventListener('click', async () => {
|
document.getElementById('copy-share-btn')?.addEventListener('click', async () => {
|
||||||
const shareUrl = document.getElementById('generated-share-url').value;
|
const shareUrl = /** @type {HTMLInputElement | null} */ (document.getElementById('generated-share-url'))?.value;
|
||||||
await fileSharing.copyLinkToClipboard(shareUrl);
|
if (shareUrl) await fileSharing.copyLinkToClipboard(shareUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('notify-share-btn').addEventListener('click', () => {
|
document.getElementById('notify-share-btn')?.addEventListener('click', () => {
|
||||||
const shareUrl = document.getElementById('generated-share-url').value;
|
const shareUrl = /** @type {HTMLInputElement | null} */ (document.getElementById('generated-share-url'))?.value;
|
||||||
contextMenus.showEmailNotificationDialog(shareUrl);
|
if (shareUrl) contextMenus.showEmailNotificationDialog(shareUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIXME make generic function (close all dialog / etc)
|
// FIXME make generic function (close all dialog / etc)
|
||||||
@@ -301,11 +299,11 @@ const ui = {
|
|||||||
document.body.appendChild(notificationDialog);
|
document.body.appendChild(notificationDialog);
|
||||||
|
|
||||||
// Add event listeners for notification dialog
|
// Add event listeners for notification dialog
|
||||||
document.getElementById('notification-cancel-btn').addEventListener('click', () => {
|
document.getElementById('notification-cancel-btn')?.addEventListener('click', () => {
|
||||||
contextMenus.closeNotificationDialog();
|
contextMenus.closeNotificationDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('notification-send-btn').addEventListener('click', () => {
|
document.getElementById('notification-send-btn')?.addEventListener('click', () => {
|
||||||
contextMenus.sendShareNotification();
|
contextMenus.sendShareNotification();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -332,7 +330,7 @@ const ui = {
|
|||||||
`;
|
`;
|
||||||
document.body.appendChild(playlistDialog);
|
document.body.appendChild(playlistDialog);
|
||||||
|
|
||||||
document.getElementById('playlist-cancel-btn').addEventListener('click', () => {
|
document.getElementById('playlist-cancel-btn')?.addEventListener('click', () => {
|
||||||
if (contextMenus) contextMenus.closePlaylistDialog();
|
if (contextMenus) contextMenus.closePlaylistDialog();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -373,9 +371,9 @@ const ui = {
|
|||||||
entry.file(
|
entry.file(
|
||||||
(file) => {
|
(file) => {
|
||||||
out.push({ file, relativePath: `${prefix}${file.name}` });
|
out.push({ file, relativePath: `${prefix}${file.name}` });
|
||||||
resolve();
|
resolve(undefined);
|
||||||
},
|
},
|
||||||
() => resolve()
|
() => resolve(undefined)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -407,20 +405,26 @@ const ui = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Dropzone events
|
// Dropzone events
|
||||||
dropzone.addEventListener('dragover', (e) => {
|
dropzone?.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropzone.classList.add('active');
|
dropzone.classList.add('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropzone.addEventListener('dragleave', () => {
|
dropzone?.addEventListener('dragleave', () => {
|
||||||
dropzone.classList.remove('active');
|
dropzone.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropzone.addEventListener('drop', async (e) => {
|
// remove previous hack e._oxiHandled
|
||||||
|
// WeakSet will automatically garbage collect entry
|
||||||
|
const handledDropEvents = new WeakSet();
|
||||||
|
|
||||||
|
dropzone?.addEventListener('drop', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation(); // Prevent bubbling to document's drop handler (avoids double upload)
|
e.stopPropagation(); // Prevent bubbling to document's drop handler (avoids double upload)
|
||||||
e._oxiHandled = true; // Mark as handled for document-level fallback
|
handledDropEvents.add(e); // Mark as handled for document-level fallback
|
||||||
dropzone.classList.remove('active');
|
dropzone.classList.remove('active');
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
if (e.dataTransfer.files.length > 0) {
|
if (e.dataTransfer.files.length > 0) {
|
||||||
// First try directory-aware extraction (Finder folder drag & drop)
|
// First try directory-aware extraction (Finder folder drag & drop)
|
||||||
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
||||||
@@ -453,6 +457,7 @@ const ui = {
|
|||||||
// Document-wide drag and drop
|
// Document-wide drag and drop
|
||||||
document.addEventListener('dragover', (e) => {
|
document.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
if (e.dataTransfer.types.includes('Files')) {
|
if (e.dataTransfer.types.includes('Files')) {
|
||||||
dropzone?.classList.remove('hidden');
|
dropzone?.classList.remove('hidden');
|
||||||
dropzone?.classList.add('active');
|
dropzone?.classList.add('active');
|
||||||
@@ -461,9 +466,9 @@ const ui = {
|
|||||||
|
|
||||||
document.addEventListener('dragleave', (e) => {
|
document.addEventListener('dragleave', (e) => {
|
||||||
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||||
dropzone.classList.remove('active');
|
dropzone?.classList.remove('active');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!dropzone.classList.contains('active')) {
|
if (!dropzone?.classList.contains('active')) {
|
||||||
dropzone?.classList.add('hidden');
|
dropzone?.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -472,11 +477,12 @@ const ui = {
|
|||||||
|
|
||||||
document.addEventListener('drop', async (e) => {
|
document.addEventListener('drop', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropzone.classList.remove('active');
|
dropzone?.classList.remove('active');
|
||||||
|
|
||||||
// Skip if already handled by the dropzone handler (defensive against bubble leaks)
|
// Skip if already handled by the dropzone handler (defensive against bubble leaks)
|
||||||
if (e._oxiHandled) return;
|
if (handledDropEvents.has(e)) return;
|
||||||
|
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
if (e.dataTransfer.files.length > 0) {
|
if (e.dataTransfer.files.length > 0) {
|
||||||
// First try directory-aware extraction (Finder folder drag & drop)
|
// First try directory-aware extraction (Finder folder drag & drop)
|
||||||
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
||||||
@@ -539,7 +545,9 @@ const ui = {
|
|||||||
*/
|
*/
|
||||||
updateBreadcrumb() {
|
updateBreadcrumb() {
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
|
if (breadcrumb) {
|
||||||
breadcrumb.innerHTML = '';
|
breadcrumb.innerHTML = '';
|
||||||
|
}
|
||||||
const path = app.breadcrumbPath; // [{id, name}, ...]
|
const path = app.breadcrumbPath; // [{id, name}, ...]
|
||||||
|
|
||||||
// -- Home icon (always present, clickable to go to root) --
|
// -- Home icon (always present, clickable to go to root) --
|
||||||
@@ -558,7 +566,7 @@ const ui = {
|
|||||||
loadFiles();
|
loadFiles();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
breadcrumb.appendChild(homeIcon);
|
breadcrumb?.appendChild(homeIcon);
|
||||||
|
|
||||||
// -- Root/Home folder name (if available) is always the first element of the breadcrumb --
|
// -- Root/Home folder name (if available) is always the first element of the breadcrumb --
|
||||||
// TODO clarify the difference between homeIcon & this first element
|
// TODO clarify the difference between homeIcon & this first element
|
||||||
@@ -579,7 +587,7 @@ const ui = {
|
|||||||
const separator = document.createElement('span');
|
const separator = document.createElement('span');
|
||||||
separator.className = 'breadcrumb-separator';
|
separator.className = 'breadcrumb-separator';
|
||||||
separator.textContent = '>';
|
separator.textContent = '>';
|
||||||
breadcrumb.appendChild(separator);
|
breadcrumb?.appendChild(separator);
|
||||||
|
|
||||||
// Segment item
|
// Segment item
|
||||||
const item = document.createElement('span');
|
const item = document.createElement('span');
|
||||||
@@ -600,7 +608,7 @@ const ui = {
|
|||||||
// can drag files on this folder
|
// can drag files on this folder
|
||||||
// dragover – only folders are valid drop targets
|
// dragover – only folders are valid drop targets
|
||||||
item.addEventListener('dragover', (e) => {
|
item.addEventListener('dragover', (e) => {
|
||||||
const card = e.target.closest('span');
|
const card = /** @type {HTMLElement} */ (e.target).closest('span');
|
||||||
if (!card?.dataset.folderId) return;
|
if (!card?.dataset.folderId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
card.classList.add('drop-target');
|
card.classList.add('drop-target');
|
||||||
@@ -609,14 +617,14 @@ const ui = {
|
|||||||
// dragleave
|
// dragleave
|
||||||
item.addEventListener('dragleave', (e) => {
|
item.addEventListener('dragleave', (e) => {
|
||||||
console.log('dragleave ', e);
|
console.log('dragleave ', e);
|
||||||
const card = e.target.closest('span');
|
const card = /** @type {HTMLElement} */ (e.target).closest('span');
|
||||||
if (!card?.dataset.folderId) return;
|
if (!card?.dataset.folderId) return;
|
||||||
card.classList.remove('drop-target');
|
card.classList.remove('drop-target');
|
||||||
});
|
});
|
||||||
|
|
||||||
// drop – only folders accept drops
|
// drop – only folders accept drops
|
||||||
item.addEventListener('drop', async (e) => {
|
item.addEventListener('drop', async (e) => {
|
||||||
const card = e.target.closest('span');
|
const card = /** @type {HTMLElement} */ (e.target).closest('span');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const targetFolderId = card.dataset.folderId;
|
const targetFolderId = card.dataset.folderId;
|
||||||
if (!targetFolderId) return;
|
if (!targetFolderId) return;
|
||||||
@@ -625,13 +633,15 @@ const ui = {
|
|||||||
card.classList.remove('drop-target');
|
card.classList.remove('drop-target');
|
||||||
|
|
||||||
const action = e.dataTransfer?.dropEffect;
|
const action = e.dataTransfer?.dropEffect;
|
||||||
|
if (action) {
|
||||||
await this._dropToFolder(action, targetFolderId, e.dataTransfer);
|
await this._dropToFolder(action, targetFolderId, e.dataTransfer);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Last segment: current location, not clickable
|
// Last segment: current location, not clickable
|
||||||
item.classList.add('breadcrumb-current');
|
item.classList.add('breadcrumb-current');
|
||||||
}
|
}
|
||||||
breadcrumb.appendChild(item);
|
breadcrumb?.appendChild(item);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -915,7 +925,7 @@ const ui = {
|
|||||||
parent_id: card.dataset.parentId || ''
|
parent_id: card.dataset.parentId || ''
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const fileData = info.data || self._items.get(info.id);
|
const fileData = info.data || this._items.get(info.id);
|
||||||
app.contextMenuTargetFile = {
|
app.contextMenuTargetFile = {
|
||||||
id: info.id,
|
id: info.id,
|
||||||
name: card.dataset.fileName,
|
name: card.dataset.fileName,
|
||||||
@@ -927,27 +937,27 @@ const ui = {
|
|||||||
|
|
||||||
// ── click (open / navigate; select only via checkbox) ──
|
// ── click (open / navigate; select only via checkbox) ──
|
||||||
filesList.addEventListener('click', (e) => {
|
filesList.addEventListener('click', (e) => {
|
||||||
const card = e.target.closest('.file-item');
|
const card = /** @type {HTMLElement} */ (e.target).closest('.file-item');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
|
|
||||||
if (e.target.closest('.file-actions')) {
|
if (/** @type {HTMLElement} */ (e.target).closest('.file-actions')) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const info = itemInfo(card);
|
const info = itemInfo(card);
|
||||||
if (!info) return;
|
if (!info) return;
|
||||||
setContextTarget(card, info);
|
setContextTarget(card, info);
|
||||||
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
|
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
|
||||||
showContextMenuAtElement(e.target.closest('.file-actions'), menuId);
|
showContextMenuAtElement(/** @type {HTMLElement} */ (e.target).closest('.file-actions'), menuId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.target.closest('.checkbox-cell')) {
|
if (/** @type {HTMLElement} */ (e.target).closest('.checkbox-cell')) {
|
||||||
toggleCardSelection(card, e);
|
toggleCardSelection(card, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Favorite star – handled by direct onclick on the button
|
// Favorite star – handled by direct onclick on the button
|
||||||
if (e.target.closest('.favorite-star')) return;
|
if (/** @type {HTMLElement} */ (e.target).closest('.favorite-star')) return;
|
||||||
|
|
||||||
// Single-click opens/navigates (selection is only via checkbox)
|
// Single-click opens/navigates (selection is only via checkbox)
|
||||||
const info = itemInfo(card);
|
const info = itemInfo(card);
|
||||||
@@ -984,7 +994,7 @@ const ui = {
|
|||||||
// ── shared events ──────────────────────
|
// ── shared events ──────────────────────
|
||||||
|
|
||||||
filesList.addEventListener('contextmenu', (e) => {
|
filesList.addEventListener('contextmenu', (e) => {
|
||||||
const card = e.target.closest('.file-item');
|
const card = /** @type {HTMLElement} */ (e.target).closest('.file-item');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const info = itemInfo(card);
|
const info = itemInfo(card);
|
||||||
@@ -1001,14 +1011,16 @@ const ui = {
|
|||||||
if (contextMenus && typeof contextMenus.syncAddToPlaylistOption === 'function') {
|
if (contextMenus && typeof contextMenus.syncAddToPlaylistOption === 'function') {
|
||||||
contextMenus.syncAddToPlaylistOption();
|
contextMenus.syncAddToPlaylistOption();
|
||||||
}
|
}
|
||||||
|
if (menu) {
|
||||||
menu.style.left = `${e.pageX}px`;
|
menu.style.left = `${e.pageX}px`;
|
||||||
menu.style.top = `${e.pageY}px`;
|
menu.style.top = `${e.pageY}px`;
|
||||||
menu?.classList.remove('hidden');
|
menu.classList.remove('hidden');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// dragstart
|
// dragstart
|
||||||
filesList.addEventListener('dragstart', (e) => {
|
filesList.addEventListener('dragstart', (e) => {
|
||||||
const card = e.target.closest('.file-item');
|
const card = /** @type {HTMLElement} */ (e.target).closest('.file-item');
|
||||||
if (!card) {
|
if (!card) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -1020,6 +1032,8 @@ const ui = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
e.dataTransfer.setData('text/plain', info.id);
|
e.dataTransfer.setData('text/plain', info.id);
|
||||||
if (info.type === 'folder') {
|
if (info.type === 'folder') {
|
||||||
e.dataTransfer.setData('application/oxicloud-folder', 'true');
|
e.dataTransfer.setData('application/oxicloud-folder', 'true');
|
||||||
@@ -1076,7 +1090,7 @@ const ui = {
|
|||||||
|
|
||||||
// if more than maxElements display the fading
|
// if more than maxElements display the fading
|
||||||
if (selectedCardFromList.length > maxElements) {
|
if (selectedCardFromList.length > maxElements) {
|
||||||
lastItemDiv.classList.add('fading');
|
lastItemDiv?.classList.add('fading');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dragPreview.appendChild(this.draggedItems);
|
this.dragPreview.appendChild(this.draggedItems);
|
||||||
@@ -1093,7 +1107,7 @@ const ui = {
|
|||||||
|
|
||||||
// dragover – only folders are valid drop targets
|
// dragover – only folders are valid drop targets
|
||||||
filesList.addEventListener('dragover', (e) => {
|
filesList.addEventListener('dragover', (e) => {
|
||||||
const card = e.target.closest('.file-item');
|
const card = /** @type {HTMLElement} */ (/** @type {HTMLElement} */ (e.target).closest('.file-item'));
|
||||||
if (!card || card.dataset.fileId) return;
|
if (!card || card.dataset.fileId) return;
|
||||||
if (!card.dataset.folderId) return;
|
if (!card.dataset.folderId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1102,14 +1116,14 @@ const ui = {
|
|||||||
|
|
||||||
// dragleave
|
// dragleave
|
||||||
filesList.addEventListener('dragleave', (e) => {
|
filesList.addEventListener('dragleave', (e) => {
|
||||||
const card = e.target.closest('.file-item');
|
const card = /** @type {HTMLElement} */ (/** @type {HTMLElement} */ (e.target).closest('.file-item'));
|
||||||
if (!card || card.dataset.fileId) return;
|
if (!card || card.dataset.fileId) return;
|
||||||
card.classList.remove('drop-target');
|
card.classList.remove('drop-target');
|
||||||
});
|
});
|
||||||
|
|
||||||
// drop – only folders accept drops
|
// drop – only folders accept drops
|
||||||
filesList.addEventListener('drop', async (e) => {
|
filesList.addEventListener('drop', async (e) => {
|
||||||
const card = e.target.closest('.file-item');
|
const card = /** @type {HTMLElement} */ (/** @type {HTMLElement} */ (e.target).closest('.file-item'));
|
||||||
if (!card || card.dataset.fileId) return;
|
if (!card || card.dataset.fileId) return;
|
||||||
const targetFolderId = card.dataset.folderId;
|
const targetFolderId = card.dataset.folderId;
|
||||||
if (!targetFolderId) return;
|
if (!targetFolderId) return;
|
||||||
@@ -1117,6 +1131,7 @@ const ui = {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
card.classList.remove('drop-target');
|
card.classList.remove('drop-target');
|
||||||
|
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
const action = e.dataTransfer.dropEffect;
|
const action = e.dataTransfer.dropEffect;
|
||||||
await this._dropToFolder(action, targetFolderId, e.dataTransfer);
|
await this._dropToFolder(action, targetFolderId, e.dataTransfer);
|
||||||
});
|
});
|
||||||
@@ -1203,7 +1218,7 @@ const ui = {
|
|||||||
const targetPath = isFavorite ? filledPath : outlinePath;
|
const targetPath = isFavorite ? filledPath : outlinePath;
|
||||||
if (svg && targetPath) {
|
if (svg && targetPath) {
|
||||||
const p = svg.querySelector('path');
|
const p = svg.querySelector('path');
|
||||||
if (p) p.setAttribute('d', targetPath[1]);
|
if (p) p.setAttribute('d', String(targetPath[1]));
|
||||||
svg.setAttribute('viewBox', `0 0 ${targetPath[0]} 512`);
|
svg.setAttribute('viewBox', `0 0 ${targetPath[0]} 512`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1379,6 +1394,8 @@ const ui = {
|
|||||||
/**
|
/**
|
||||||
* Render an array of folders into both grid and list views
|
* Render an array of folders into both grid and list views
|
||||||
* using DocumentFragment for minimal reflows.
|
* using DocumentFragment for minimal reflows.
|
||||||
|
*
|
||||||
|
* @param {FolderInfo[]} folders
|
||||||
*/
|
*/
|
||||||
renderFolders(folders) {
|
renderFolders(folders) {
|
||||||
if (!this._delegationReady) this.initDelegation();
|
if (!this._delegationReady) this.initDelegation();
|
||||||
@@ -1503,6 +1520,8 @@ function showContextMenuAtElement(triggerElement, menuId) {
|
|||||||
menu.classList.remove('hidden');
|
menu.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let __rubberBandJustFinished = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rubber band (lasso) selection — click + drag on empty grid area
|
* Rubber band (lasso) selection — click + drag on empty grid area
|
||||||
* to draw a rectangle and select all cards it touches.
|
* to draw a rectangle and select all cards it touches.
|
||||||
@@ -1635,7 +1654,7 @@ if (document.readyState === 'loading') {
|
|||||||
* @param {boolean} [options.danger=false] - Use danger styling (red)
|
* @param {boolean} [options.danger=false] - Use danger styling (red)
|
||||||
* @returns {Promise<boolean>} true if confirmed, false if cancelled
|
* @returns {Promise<boolean>} true if confirmed, false if cancelled
|
||||||
*/
|
*/
|
||||||
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
|
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true }) {
|
||||||
const ct = confirmText || i18n.t('actions.delete');
|
const ct = confirmText || i18n.t('actions.delete');
|
||||||
const cc = cancelText || i18n.t('actions.cancel');
|
const cc = cancelText || i18n.t('actions.cancel');
|
||||||
const t = title || i18n.t('dialogs.confirm_title');
|
const t = title || i18n.t('dialogs.confirm_title');
|
||||||
@@ -1674,8 +1693,8 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t
|
|||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
overlay.querySelector('.confirm-dialog-cancel').addEventListener('click', () => cleanup(false));
|
overlay.querySelector('.confirm-dialog-cancel')?.addEventListener('click', () => cleanup(false));
|
||||||
overlay.querySelector('.confirm-dialog-ok').addEventListener('click', () => cleanup(true));
|
overlay.querySelector('.confirm-dialog-ok')?.addEventListener('click', () => cleanup(true));
|
||||||
overlay.addEventListener('click', (e) => {
|
overlay.addEventListener('click', (e) => {
|
||||||
if (e.target === overlay) cleanup(false);
|
if (e.target === overlay) cleanup(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,21 +5,10 @@
|
|||||||
|
|
||||||
import { isTextViewable } from '../core/formatters.js';
|
import { isTextViewable } from '../core/formatters.js';
|
||||||
|
|
||||||
const uiFileTypes = {
|
/** @import {FileInfo} from '../core/types.js' */
|
||||||
// TODO: 'd better to use a canViw() method in inlineViewer
|
|
||||||
isViewableFile(file) {
|
|
||||||
if (!file?.mime_type) return false;
|
|
||||||
if (file.mime_type.startsWith('image/')) return true;
|
|
||||||
if (file.mime_type === 'application/pdf') return true;
|
|
||||||
if (file.mime_type.startsWith('audio/')) return true;
|
|
||||||
if (file.mime_type.startsWith('video/')) return true;
|
|
||||||
return isTextViewable(file.mime_type);
|
|
||||||
},
|
|
||||||
|
|
||||||
getIconClass(fileName) {
|
/** @type {Record<string, string>} */
|
||||||
if (!fileName) return 'fas fa-file';
|
const ICON_CLASS_MAP = {
|
||||||
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
|
||||||
const map = {
|
|
||||||
pdf: 'fas fa-file-pdf',
|
pdf: 'fas fa-file-pdf',
|
||||||
doc: 'fas fa-file-word',
|
doc: 'fas fa-file-word',
|
||||||
docx: 'fas fa-file-word',
|
docx: 'fas fa-file-word',
|
||||||
@@ -71,14 +60,10 @@ const uiFileTypes = {
|
|||||||
bash: 'fas fa-terminal',
|
bash: 'fas fa-terminal',
|
||||||
bat: 'fas fa-terminal',
|
bat: 'fas fa-terminal',
|
||||||
md: 'fas fa-file-alt'
|
md: 'fas fa-file-alt'
|
||||||
};
|
};
|
||||||
return map[ext] || 'fas fa-file';
|
|
||||||
},
|
|
||||||
|
|
||||||
getIconSpecialClass(fileName) {
|
/** @type {Record<string, string>} */
|
||||||
if (!fileName) return '';
|
const ICON_SPECIAL_CLASS_MAP = {
|
||||||
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
|
||||||
const map = {
|
|
||||||
pdf: 'pdf-icon',
|
pdf: 'pdf-icon',
|
||||||
doc: 'doc-icon',
|
doc: 'doc-icon',
|
||||||
docx: 'doc-icon',
|
docx: 'doc-icon',
|
||||||
@@ -167,8 +152,46 @@ const uiFileTypes = {
|
|||||||
bat: 'script-icon',
|
bat: 'script-icon',
|
||||||
md: 'code-icon md-icon',
|
md: 'code-icon md-icon',
|
||||||
txt: 'doc-icon'
|
txt: 'doc-icon'
|
||||||
};
|
};
|
||||||
return map[ext] || '';
|
|
||||||
|
const uiFileTypes = {
|
||||||
|
// TODO: 'd better to use a canViw() method in inlineViewer
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {FileInfo} file
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isViewableFile(file) {
|
||||||
|
if (!file?.mime_type) return false;
|
||||||
|
if (file.mime_type.startsWith('image/')) return true;
|
||||||
|
if (file.mime_type === 'application/pdf') return true;
|
||||||
|
if (file.mime_type.startsWith('audio/')) return true;
|
||||||
|
if (file.mime_type.startsWith('video/')) return true;
|
||||||
|
return isTextViewable(file.mime_type);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} fileName
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getIconClass(fileName) {
|
||||||
|
if (!fileName) return 'fas fa-file';
|
||||||
|
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
||||||
|
|
||||||
|
return ICON_CLASS_MAP[ext] || 'fas fa-file';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} fileName
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getIconSpecialClass(fileName) {
|
||||||
|
if (!fileName) return '';
|
||||||
|
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
||||||
|
|
||||||
|
return ICON_SPECIAL_CLASS_MAP[ext] || '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
import { notifications } from '../core/notifications.js';
|
import { notifications } from '../core/notifications.js';
|
||||||
|
|
||||||
const uiNotifications = {
|
const uiNotifications = {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} message
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
show(title, message) {
|
show(title, message) {
|
||||||
const normalizedTitle = String(title || '').toLowerCase();
|
const normalizedTitle = String(title || '').toLowerCase();
|
||||||
let icon = 'fa-info-circle';
|
let icon = 'fa-info-circle';
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function setupUserMenu() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (wrapper.classList.contains('open') && !wrapper.contains(e.target)) {
|
if (wrapper.classList.contains('open') && !wrapper.contains(/** @type {Node|null} */ (e.target))) {
|
||||||
wrapper.classList.remove('open');
|
wrapper.classList.remove('open');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -137,7 +137,7 @@ function setupUserMenu() {
|
|||||||
const aboutOverlay = document.getElementById('about-modal-overlay');
|
const aboutOverlay = document.getElementById('about-modal-overlay');
|
||||||
if (aboutCloseBtn) {
|
if (aboutCloseBtn) {
|
||||||
aboutCloseBtn.addEventListener('click', () => {
|
aboutCloseBtn.addEventListener('click', () => {
|
||||||
aboutOverlay.classList.add('hidden');
|
aboutOverlay?.classList.add('hidden');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (aboutOverlay) {
|
if (aboutOverlay) {
|
||||||
@@ -242,7 +242,7 @@ function showUserProfileModal() {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Set dynamic bar width and color via JS property (CSP-safe)
|
// Set dynamic bar width and color via JS property (CSP-safe)
|
||||||
const barFill = overlay.querySelector('#about-bar-fill');
|
const barFill = /** @type {HTMLDivElement} */ (overlay.querySelector('#about-bar-fill'));
|
||||||
if (barFill) {
|
if (barFill) {
|
||||||
barFill.style.width = `${percentage}%`;
|
barFill.style.width = `${percentage}%`;
|
||||||
barFill.style.background = barColor;
|
barFill.style.background = barColor;
|
||||||
@@ -251,7 +251,7 @@ function showUserProfileModal() {
|
|||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
requestAnimationFrame(() => overlay.classList.add('show'));
|
requestAnimationFrame(() => overlay.classList.add('show'));
|
||||||
|
|
||||||
overlay.querySelector('#profile-modal-close').addEventListener('click', () => {
|
overlay.querySelector('#profile-modal-close')?.addEventListener('click', () => {
|
||||||
overlay.classList.remove('show');
|
overlay.classList.remove('show');
|
||||||
setTimeout(() => overlay.remove(), 200);
|
setTimeout(() => overlay.remove(), 200);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,11 +3,21 @@
|
|||||||
* Centralized global helpers for date/size/text formatting and XSS-safe escaping.
|
* Centralized global helpers for date/size/text formatting and XSS-safe escaping.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
if (typeof str !== 'string') return '';
|
if (typeof str !== 'string') return '';
|
||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} bytes
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function formatFileSize(bytes) {
|
function formatFileSize(bytes) {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
@@ -19,11 +29,21 @@ function formatFileSize(bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Formats a byte count for quota display. When bytes is 0, returns "∞" (unlimited).
|
/// Formats a byte count for quota display. When bytes is 0, returns "∞" (unlimited).
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} bytes
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function formatQuotaSize(bytes) {
|
function formatQuotaSize(bytes) {
|
||||||
if (bytes === 0) return '∞';
|
if (bytes === 0) return '∞';
|
||||||
return formatFileSize(bytes);
|
return formatFileSize(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Date | number| null} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
let dateValue;
|
let dateValue;
|
||||||
@@ -38,6 +58,11 @@ function formatDateTime(value) {
|
|||||||
return `${dateValue.toLocaleDateString()} ${dateValue.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
return `${dateValue.toLocaleDateString()} ${dateValue.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Date | number| null} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function formatDateShort(value) {
|
function formatDateShort(value) {
|
||||||
if (!value) return 'N/A';
|
if (!value) return 'N/A';
|
||||||
const dateValue = typeof value === 'number' ? new Date(value * 1000) : new Date(value);
|
const dateValue = typeof value === 'number' ? new Date(value * 1000) : new Date(value);
|
||||||
@@ -49,10 +74,7 @@ function formatDateShort(value) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTextViewable(mimeType) {
|
const TEXT_TYPES = [
|
||||||
if (!mimeType) return false;
|
|
||||||
if (mimeType.startsWith('text/')) return true;
|
|
||||||
const textTypes = [
|
|
||||||
'application/json',
|
'application/json',
|
||||||
'application/xml',
|
'application/xml',
|
||||||
'application/javascript',
|
'application/javascript',
|
||||||
@@ -61,8 +83,18 @@ function isTextViewable(mimeType) {
|
|||||||
'application/toml',
|
'application/toml',
|
||||||
'application/x-toml',
|
'application/x-toml',
|
||||||
'application/sql'
|
'application/sql'
|
||||||
];
|
];
|
||||||
return textTypes.includes(mimeType);
|
// FIXME: move is to another file
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} mimeType
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isTextViewable(mimeType) {
|
||||||
|
if (!mimeType) return false;
|
||||||
|
if (mimeType.startsWith('text/')) return true;
|
||||||
|
|
||||||
|
return TEXT_TYPES.includes(mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { escapeHtml, formatDateShort, formatDateTime, formatFileSize, formatQuotaSize, isTextViewable };
|
export { escapeHtml, formatDateShort, formatDateTime, formatFileSize, formatQuotaSize, isTextViewable };
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ if (!supportedLocales.includes(currentLocale)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache for translations
|
// Cache for translations
|
||||||
|
/** @type {Record<string, Object>} */
|
||||||
const translations = {};
|
const translations = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ function replaceIconsInElement(container) {
|
|||||||
if (!container) container = document.body;
|
if (!container) container = document.body;
|
||||||
const icons = container.querySelectorAll('i[class*="fa-"]');
|
const icons = container.querySelectorAll('i[class*="fa-"]');
|
||||||
for (let i = 0; i < icons.length; i++) {
|
for (let i = 0; i < icons.length; i++) {
|
||||||
const el = icons[i];
|
const el = /** @type {HTMLElement} */ (icons[i]);
|
||||||
const classes = el.className.split(/\s+/);
|
const classes = el.className.split(/\s+/);
|
||||||
|
|
||||||
// Find the icon name (fa-xxx)
|
// Find the icon name (fa-xxx)
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function createLanguageSelector(containerId = 'language-selector') {
|
|||||||
option.className = `language-option${lang.code === currentLocale ? ' active' : ''}`;
|
option.className = `language-option${lang.code === currentLocale ? ' active' : ''}`;
|
||||||
option.setAttribute('role', 'option');
|
option.setAttribute('role', 'option');
|
||||||
option.setAttribute('data-lang', lang.code);
|
option.setAttribute('data-lang', lang.code);
|
||||||
option.setAttribute('aria-selected', lang.code === currentLocale);
|
option.setAttribute('aria-selected', String(lang.code === currentLocale));
|
||||||
option.innerHTML = `
|
option.innerHTML = `
|
||||||
<span class="lang-flag">${lang.flag}</span>
|
<span class="lang-flag">${lang.flag}</span>
|
||||||
<span class="lang-name">${lang.name}</span>
|
<span class="lang-name">${lang.name}</span>
|
||||||
@@ -134,7 +134,7 @@ function createLanguageSelector(containerId = 'language-selector') {
|
|||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!container.contains(e.target)) {
|
if (!container.contains(/** @type {Node | null } */ (e.target))) {
|
||||||
closeDropdown(container);
|
closeDropdown(container);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ const Modal = {
|
|||||||
this.closeBtn = document.getElementById('modal-close-btn');
|
this.closeBtn = document.getElementById('modal-close-btn');
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
this.cancelBtn.addEventListener('click', () => this.close(false));
|
this.cancelBtn?.addEventListener('click', () => this.close(false));
|
||||||
this.closeBtn.addEventListener('click', () => this.close(false));
|
this.closeBtn?.addEventListener('click', () => this.close(false));
|
||||||
this.confirmBtn.addEventListener('click', () => this.confirm());
|
this.confirmBtn?.addEventListener('click', () => this.confirm());
|
||||||
|
|
||||||
// Close on overlay click
|
// Close on overlay click
|
||||||
this.overlay.addEventListener('click', (e) => {
|
this.overlay.addEventListener('click', (e) => {
|
||||||
@@ -65,7 +65,7 @@ const Modal = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle Enter and Escape keys
|
// Handle Enter and Escape keys
|
||||||
this.input.addEventListener('keydown', (e) => {
|
this.input?.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.confirm();
|
this.confirm();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const notifications = (() => {
|
|||||||
|
|
||||||
bellBtn.addEventListener('click', (e) => {
|
bellBtn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const open = wrapper.classList.toggle('open');
|
const open = wrapper?.classList.toggle('open');
|
||||||
bellBtn.classList.toggle('active', open);
|
bellBtn.classList.toggle('active', open);
|
||||||
|
|
||||||
// Close user-menu if it's open
|
// Close user-menu if it's open
|
||||||
@@ -46,7 +46,7 @@ const notifications = (() => {
|
|||||||
|
|
||||||
// Close on outside click
|
// Close on outside click
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!wrapper.contains(e.target)) {
|
if (!wrapper?.contains(/** @type {Node | null} */ (e.target))) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -63,8 +63,8 @@ const notifications = (() => {
|
|||||||
function close() {
|
function close() {
|
||||||
const bellBtn = $('notif-bell-btn');
|
const bellBtn = $('notif-bell-btn');
|
||||||
const wrapper = $('notif-wrapper');
|
const wrapper = $('notif-wrapper');
|
||||||
wrapper.classList.remove('open');
|
wrapper?.classList.remove('open');
|
||||||
bellBtn.classList.remove('active');
|
bellBtn?.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── badge helpers ──────────────────────────────────────── */
|
/* ── badge helpers ──────────────────────────────────────── */
|
||||||
@@ -82,7 +82,7 @@ const notifications = (() => {
|
|||||||
if (!badge) return;
|
if (!badge) return;
|
||||||
if (_badgeCount > 0) {
|
if (_badgeCount > 0) {
|
||||||
badge.classList.remove('hidden');
|
badge.classList.remove('hidden');
|
||||||
badge.textContent = _badgeCount > 99 ? '99+' : _badgeCount;
|
badge.textContent = _badgeCount > 99 ? '99+' : String(_badgeCount);
|
||||||
} else {
|
} else {
|
||||||
badge.classList.add('hidden');
|
badge.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {Object} FolderInfo
|
||||||
|
* @property {string} category
|
||||||
|
* @property {number} created_at - timestamp
|
||||||
|
* @property {string} icon_class
|
||||||
|
* @property {string} icon_special_class
|
||||||
|
* @property {string} id the uniq id of the folder
|
||||||
|
* @property {boolean} is_root
|
||||||
|
* @property {number} modified_at
|
||||||
|
* @property {string} name
|
||||||
|
* @property {string} owner_id
|
||||||
|
* @property {string|null} parent_id the folder parent (null if is_root)
|
||||||
|
* @property {string} path the full path
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} FileInfo
|
||||||
|
* @property {string} category
|
||||||
|
* @property {number} created_at - timestamp
|
||||||
|
* @property {string} icon_class
|
||||||
|
* @property {string} icon_special_class
|
||||||
|
* @property {string} id the uniq id of the folder
|
||||||
|
* @property {string} mime_type
|
||||||
|
* @property {number} modified_at - timestamp
|
||||||
|
* @property {string} name
|
||||||
|
* @property {string} owner_id
|
||||||
|
* @property {string} folder_id the folder parent
|
||||||
|
* @property {string} path the full path
|
||||||
|
* @property {number} size
|
||||||
|
* @property {string} size_formatted
|
||||||
|
* @property {number} sort_date
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} SharePermissions
|
||||||
|
* @property {boolean} read
|
||||||
|
* @property {boolean} reshare
|
||||||
|
* @property {boolean} write
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Share
|
||||||
|
* @property {number} access_count
|
||||||
|
* @property {number} created_at - timestamp
|
||||||
|
* @property {String} created_by
|
||||||
|
* @property {number} expires_at - timestamp
|
||||||
|
* @property {boolean} has_password
|
||||||
|
* @property {string} id
|
||||||
|
* @property {string} item_id
|
||||||
|
* @property {string} item_name
|
||||||
|
* @property {string} item_type
|
||||||
|
* @property {SharePermissions} permissions
|
||||||
|
* @property {string | null} token
|
||||||
|
* @property {string} url
|
||||||
|
*/
|
||||||
@@ -405,7 +405,7 @@ function initLanguageSelector() {
|
|||||||
const pickerName = document.getElementById('lang-picker-name');
|
const pickerName = document.getElementById('lang-picker-name');
|
||||||
const searchInput = document.getElementById('lang-picker-search-input');
|
const searchInput = document.getElementById('lang-picker-search-input');
|
||||||
|
|
||||||
if (!languagePanel || !picker) return;
|
if (!languagePanel || !picker || !pickerFlag || !pickerName || !pickerList) return;
|
||||||
|
|
||||||
// --- Auto-detect browser language ---
|
// --- Auto-detect browser language ---
|
||||||
const detected = detectBrowserLanguage();
|
const detected = detectBrowserLanguage();
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ const fileOps = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload files to the server with real-time progress indication
|
* 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) {
|
async uploadFiles(files) {
|
||||||
const originalFiles = Array.from(files || []);
|
const originalFiles = Array.from(files || []);
|
||||||
|
|||||||
@@ -146,14 +146,14 @@ const search = {
|
|||||||
if (previousSearchHeader) {
|
if (previousSearchHeader) {
|
||||||
previousSearchHeader.replaceWith(searchHeader);
|
previousSearchHeader.replaceWith(searchHeader);
|
||||||
} else {
|
} else {
|
||||||
pageStickyHeader.appendChild(searchHeader);
|
pageStickyHeader?.appendChild(searchHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort dropdown — re-searches with new sort order (server-side)
|
// 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) {
|
if (sortSelect) {
|
||||||
sortSelect.addEventListener('change', () => {
|
sortSelect.addEventListener('change', () => {
|
||||||
const searchInput = document.querySelector('.search-container input');
|
const searchInput = /** @type {HTMLInputElement} */ (document.querySelector('.search-container input'));
|
||||||
if (searchInput?.value.trim()) {
|
if (searchInput?.value.trim()) {
|
||||||
const event = new CustomEvent('search-resort', {
|
const event = new CustomEvent('search-resort', {
|
||||||
detail: { sort_by: sortSelect.value }
|
detail: { sort_by: sortSelect.value }
|
||||||
@@ -167,11 +167,12 @@ const search = {
|
|||||||
const clearSearchBtn = document.getElementById('clear-search-btn');
|
const clearSearchBtn = document.getElementById('clear-search-btn');
|
||||||
if (clearSearchBtn) {
|
if (clearSearchBtn) {
|
||||||
clearSearchBtn.addEventListener('click', () => {
|
clearSearchBtn.addEventListener('click', () => {
|
||||||
document.querySelector('.search-container input').value = '';
|
const searchInput = /** @type {HTMLInputElement} */ (document.querySelector('.search-container input'));
|
||||||
|
searchInput.value = '';
|
||||||
app.currentPath = '';
|
app.currentPath = '';
|
||||||
app.isSearchMode = false;
|
app.isSearchMode = false;
|
||||||
document.querySelector('.search-results-header')?.remove();
|
document.querySelector('.search-results-header')?.remove();
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadFiles } from '../../app/filesView.js';
|
import { loadFiles } from '../../app/filesView.js';
|
||||||
|
import { ui } from '../../app/ui.js';
|
||||||
|
|
||||||
class WopiEditor {
|
class WopiEditor {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -46,9 +47,7 @@ class WopiEditor {
|
|||||||
window.open(hostUrl, '_blank');
|
window.open(hostUrl, '_blank');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to open WOPI editor in tab:', error);
|
console.error('Failed to open WOPI editor in tab:', error);
|
||||||
if (window.showNotification) {
|
ui.showNotification('Could not open the document editor.', 'error');
|
||||||
window.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
|
// FIXME: this case is not easy to understand, should apply better implementation
|
||||||
multiSelect.init();
|
multiSelect.init();
|
||||||
|
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb();
|
||||||
|
|
||||||
if (this._cache.size === 0) {
|
if (this._cache.size === 0) {
|
||||||
ui.showError(`
|
ui.showError(`
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ const photosView = {
|
|||||||
if (grid?.classList.contains('photos-grid')) {
|
if (grid?.classList.contains('photos-grid')) {
|
||||||
grid.insertAdjacentHTML('beforeend', tilesHtml);
|
grid.insertAdjacentHTML('beforeend', tilesHtml);
|
||||||
const countSpan = existingHeader.querySelector('.photos-day-count');
|
const countSpan = existingHeader.querySelector('.photos-day-count');
|
||||||
if (countSpan) countSpan.textContent = grid.children.length;
|
if (countSpan) countSpan.textContent = String(grid.children.length);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// New group — insert header + grid before sentinel
|
// New group — insert header + grid before sentinel
|
||||||
@@ -269,11 +269,14 @@ const photosView = {
|
|||||||
/** @param {number} [startIndex=0] When > 0, only process video tiles
|
/** @param {number} [startIndex=0] When > 0, only process video tiles
|
||||||
* for items[startIndex..] — avoids re-scanning the entire DOM. */
|
* for items[startIndex..] — avoids re-scanning the entire DOM. */
|
||||||
_setupVideoThumbnails(startIndex = 0) {
|
_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;
|
const newIds = startIndex > 0 ? new Set(this.items.slice(startIndex).map((f) => f.id)) : null;
|
||||||
|
|
||||||
|
if (!tiles) return;
|
||||||
|
|
||||||
for (const tile of tiles) {
|
for (const tile of tiles) {
|
||||||
const fileId = tile.dataset.id;
|
const fileId = tile.dataset.id;
|
||||||
|
if (!fileId) continue;
|
||||||
if (newIds && !newIds.has(fileId)) continue;
|
if (newIds && !newIds.has(fileId)) continue;
|
||||||
if (this._videoThumbCache.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
|
/** Extract a single frame from a video and display it as the tile
|
||||||
* thumbnail, then upload the JPEG to the server for caching. */
|
* thumbnail, then upload the JPEG to the server for caching. */
|
||||||
_generateVideoThumbnail(tile, img) {
|
_generateVideoThumbnail(tile, img) {
|
||||||
|
// TODO: use thumbnail.js s common lib
|
||||||
|
|
||||||
const fileId = tile.dataset.id;
|
const fileId = tile.dataset.id;
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
video.crossOrigin = 'anonymous';
|
video.crossOrigin = 'anonymous';
|
||||||
@@ -341,7 +346,7 @@ const photosView = {
|
|||||||
canvas.width = Math.round(video.videoWidth * scale);
|
canvas.width = Math.round(video.videoWidth * scale);
|
||||||
canvas.height = Math.round(video.videoHeight * scale);
|
canvas.height = Math.round(video.videoHeight * scale);
|
||||||
const ctx = canvas.getContext('2d');
|
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,
|
// JPEG: explicit quality control, universally supported,
|
||||||
// and server stores as-is when dimensions fit (zero re-encode).
|
// and server stores as-is when dimensions fit (zero re-encode).
|
||||||
@@ -362,7 +367,7 @@ const photosView = {
|
|||||||
|
|
||||||
// Upload to server for permanent caching
|
// Upload to server for permanent caching
|
||||||
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
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}`;
|
if (token) headers.Authorization = `Bearer ${token}`;
|
||||||
|
|
||||||
fetch(`/api/files/${fileId}/thumbnail/preview`, {
|
fetch(`/api/files/${fileId}/thumbnail/preview`, {
|
||||||
@@ -426,6 +431,7 @@ const photosView = {
|
|||||||
|
|
||||||
/** Render empty state */
|
/** Render empty state */
|
||||||
_renderEmpty() {
|
_renderEmpty() {
|
||||||
|
if (!this._container) return;
|
||||||
this._container.innerHTML = `
|
this._container.innerHTML = `
|
||||||
<div class="photos-empty">
|
<div class="photos-empty">
|
||||||
<i class="fas fa-images"></i>
|
<i class="fas fa-images"></i>
|
||||||
@@ -526,15 +532,20 @@ const photosView = {
|
|||||||
<button id="photos-sel-clear" title="Clear"><i class="fas fa-times"></i></button>
|
<button id="photos-sel-clear" title="Clear"><i class="fas fa-times"></i></button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
bar.querySelector('#photos-sel-clear').onclick = () => {
|
const bar_clear = /** @type {HTMLButtonElement} */ (bar.querySelector('#photos-sel-clear'));
|
||||||
|
if (bar_clear) {
|
||||||
|
bar_clear.onclick = () => {
|
||||||
this.selected.clear();
|
this.selected.clear();
|
||||||
this._container.querySelectorAll('.photo-tile.selected').forEach((t) => {
|
this._container.querySelectorAll('.photo-tile.selected').forEach((t) => {
|
||||||
t.classList.remove('selected');
|
t.classList.remove('selected');
|
||||||
});
|
});
|
||||||
this._hideSelectionBar();
|
this._hideSelectionBar();
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
bar.querySelector('#photos-sel-delete').onclick = async () => {
|
const bar_delete = /** @type {HTMLButtonElement} */ (bar.querySelector('#photos-sel-delete'));
|
||||||
|
if (bar_delete) {
|
||||||
|
bar_delete.onclick = async () => {
|
||||||
if (!confirm('Delete selected items?')) return;
|
if (!confirm('Delete selected items?')) return;
|
||||||
for (const fid of this.selected) {
|
for (const fid of this.selected) {
|
||||||
try {
|
try {
|
||||||
@@ -553,8 +564,11 @@ const photosView = {
|
|||||||
this._renderedCount = 0;
|
this._renderedCount = 0;
|
||||||
this._renderFull();
|
this._renderFull();
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
bar.querySelector('#photos-sel-download').onclick = async () => {
|
const bar_download = /** @type {HTMLButtonElement} */ (bar.querySelector('#photos-sel-download'));
|
||||||
|
if (bar_download) {
|
||||||
|
bar_download.onclick = async () => {
|
||||||
for (const fid of this.selected) {
|
for (const fid of this.selected) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = `/api/files/${fid}`;
|
a.href = `/api/files/${fid}`;
|
||||||
@@ -564,6 +578,7 @@ const photosView = {
|
|||||||
a.remove();
|
a.remove();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
bar.style.display = 'flex';
|
bar.style.display = 'flex';
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const recent = {
|
|||||||
multiSelect.clear();
|
multiSelect.clear();
|
||||||
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
||||||
}
|
}
|
||||||
ui.updateBreadcrumb('');
|
ui.updateBreadcrumb();
|
||||||
|
|
||||||
if (recentItems.length === 0) {
|
if (recentItems.length === 0) {
|
||||||
ui.showError(`
|
ui.showError(`
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ const fileSharing = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Format expiration date for display (Unix timestamp in seconds or ISO string)
|
* Format expiration date for display (Unix timestamp in seconds or ISO string)
|
||||||
* @param {number|string} value
|
* @param {number|Date} value
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
formatExpirationDate(value) {
|
formatExpirationDate(value) {
|
||||||
@@ -177,7 +177,7 @@ const fileSharing = {
|
|||||||
* Send a notification about a shared resource (stub — no backend endpoint yet)
|
* Send a notification about a shared resource (stub — no backend endpoint yet)
|
||||||
* @param {string} shareUrl
|
* @param {string} shareUrl
|
||||||
* @param {string} recipientEmail
|
* @param {string} recipientEmail
|
||||||
* @param {string} message
|
* @param {string} _message
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async sendShareNotification(shareUrl, recipientEmail, _message = '') {
|
async sendShareNotification(shareUrl, recipientEmail, _message = '') {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getCsrfHeaders } from '../../core/csrf.js';
|
|||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
var API_BASE = window.location.origin;
|
var API_BASE = window.location.origin;
|
||||||
var codeInput = document.getElementById('user-code');
|
var codeInput = /** @type {HTMLInputElement} */ (document.getElementById('user-code'));
|
||||||
var deviceInfo = document.getElementById('device-info');
|
var deviceInfo = document.getElementById('device-info');
|
||||||
var actionButtons = document.getElementById('action-buttons');
|
var actionButtons = document.getElementById('action-buttons');
|
||||||
var errorText = document.getElementById('error-text');
|
var errorText = document.getElementById('error-text');
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ var errorTitle = document.getElementById('error-title');
|
|||||||
var errorMessage = document.getElementById('error-message');
|
var errorMessage = document.getElementById('error-message');
|
||||||
var errorAction = document.getElementById('error-action');
|
var errorAction = document.getElementById('error-action');
|
||||||
|
|
||||||
|
if (!errorTitle || !errorMessage || !errorAction) {
|
||||||
|
throw new Error('missing html elements');
|
||||||
|
}
|
||||||
|
|
||||||
switch (errorType) {
|
switch (errorType) {
|
||||||
case 'invalid-credentials':
|
case 'invalid-credentials':
|
||||||
errorTitle.textContent = 'Login Failed';
|
errorTitle.textContent = 'Login Failed';
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ import { formatDateShort } from '../../core/formatters.js';
|
|||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { fileSharing } from '../../features/sharing/fileSharing.js';
|
import { fileSharing } from '../../features/sharing/fileSharing.js';
|
||||||
|
|
||||||
|
/** @import {Share} from '../../core/types.js' */
|
||||||
|
|
||||||
const TTL = 5 * 60 * 1000; // 5 min
|
const TTL = 5 * 60 * 1000; // 5 min
|
||||||
|
|
||||||
const sharedView = {
|
const sharedView = {
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
/** @type {Array<Share>} */
|
||||||
items: [],
|
items: [],
|
||||||
|
|
||||||
_expires: 0,
|
_expires: 0,
|
||||||
@@ -21,6 +25,7 @@ const sharedView = {
|
|||||||
/** @type {Map<string, boolean>} key = "file:<id>" | "folder:<id>" */
|
/** @type {Map<string, boolean>} key = "file:<id>" | "folder:<id>" */
|
||||||
_knownItemsId: new Map(),
|
_knownItemsId: new Map(),
|
||||||
|
|
||||||
|
/** @type {Array<Share>} */
|
||||||
filteredItems: [],
|
filteredItems: [],
|
||||||
currentItem: null,
|
currentItem: null,
|
||||||
|
|
||||||
@@ -67,7 +72,7 @@ const sharedView = {
|
|||||||
*
|
*
|
||||||
* @param {boolean} force ignore cache
|
* @param {boolean} force ignore cache
|
||||||
*/
|
*/
|
||||||
async loadItems(force) {
|
async loadItems(force = false) {
|
||||||
if (this._expires > Date.now() && !force) return;
|
if (this._expires > Date.now() && !force) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user