style(front/js): apply types on all objects
- reduce amount of warnings in IDE
- maximize API type mapping with static/js/core/types.js
This commit is contained in:
@@ -8,6 +8,14 @@ import { updateStorageUsageDisplay } from './main.js';
|
||||
import { app } from './state.js';
|
||||
import { ui } from './ui.js';
|
||||
|
||||
/**
|
||||
* @import {User} from '../core/types.js'
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<User | null>}
|
||||
*/
|
||||
async function refreshUserData() {
|
||||
const USER_DATA_KEY = 'oxicloud_user';
|
||||
|
||||
@@ -25,6 +33,7 @@ async function refreshUserData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @type {User} */
|
||||
const userData = await response.json();
|
||||
console.log('Refreshed user data from server:', userData);
|
||||
console.log('Storage from server: used=', userData.storage_used_bytes, 'quota=', userData.storage_quota_bytes);
|
||||
@@ -83,6 +92,7 @@ async function checkAuthentication() {
|
||||
// Check session validity by calling /api/auth/me (cookie auto-sent)
|
||||
console.log('Checking session via /api/auth/me...');
|
||||
|
||||
/** @type {User} */
|
||||
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
||||
if (userData.username) {
|
||||
// We have cached user data — render immediately, refresh in background
|
||||
|
||||
@@ -9,14 +9,14 @@ import { app } from './state.js';
|
||||
import { ui } from './ui.js';
|
||||
import { uiNotifications } from './uiNotifications.js';
|
||||
|
||||
/** @import {FileInfo, FolderInfo} from '../core/types.js' */
|
||||
/** @import {FileItem, FolderItem} from '../core/types.js' */
|
||||
|
||||
let isLoadingFiles = false;
|
||||
|
||||
/**
|
||||
* getFolder information
|
||||
* @param {string} id the id of the folder
|
||||
* @returns {Promise<FolderInfo>}
|
||||
* @returns {Promise<FolderItem>}
|
||||
*/
|
||||
async function getFolder(id) {
|
||||
/** @type {HeadersInit} */
|
||||
@@ -47,7 +47,7 @@ async function getFolder(id) {
|
||||
async function rebuildBreadCrumb() {
|
||||
/**
|
||||
* Store the leaf (this is the current displayed folder)
|
||||
* @type {FolderInfo | null}
|
||||
* @type {FolderItem | null}
|
||||
*/
|
||||
let currentFolderInfo = null;
|
||||
|
||||
@@ -172,7 +172,11 @@ async function loadFiles(options = { insertHistory: true }) {
|
||||
|
||||
if (forceRefresh) {
|
||||
url += `&force_refresh=true`;
|
||||
if (requestOptions.headers) requestOptions.headers['X-Force-Refresh'] = 'true';
|
||||
if (requestOptions.headers) {
|
||||
const headers = new Headers(requestOptions.headers);
|
||||
headers.set('X-Force-Refresh', 'true');
|
||||
requestOptions.headers = headers;
|
||||
}
|
||||
console.log('Forcing complete refresh ignoring cache');
|
||||
}
|
||||
|
||||
@@ -202,10 +206,10 @@ async function loadFiles(options = { insertHistory: true }) {
|
||||
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
||||
}
|
||||
|
||||
/** @type {FolderInfo[]} */
|
||||
/** @type {FolderItem[]} */
|
||||
const folderList = Array.isArray(listing.folders) ? listing.folders : [];
|
||||
|
||||
/** @type {FileInfo[]} */
|
||||
/** @type {FileItem[]} */
|
||||
const fileList = Array.isArray(listing.files) ? listing.files : [];
|
||||
|
||||
if (folderList.length === 0 && fileList.length === 0) {
|
||||
|
||||
@@ -35,6 +35,10 @@ import { loadTrashItems } from './trashView.js';
|
||||
import { ui } from './ui.js';
|
||||
import { setupUserMenu } from './userMenu.js';
|
||||
|
||||
/**
|
||||
* @import {User} from '../core/types.js'
|
||||
*/
|
||||
|
||||
// Upload dropdown listener state (prevents accumulated listeners)
|
||||
/** @type {((e: MouseEvent) => void) | null} */
|
||||
let uploadDropdownDocumentClickHandler = null;
|
||||
@@ -141,7 +145,7 @@ const ACTIONS_BAR_TEMPLATES = {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} mode
|
||||
* @param {'files' | 'trash' | 'favorites' | 'recent' | 'hidden'} mode
|
||||
* @param {boolean} [force=false]
|
||||
* @returns
|
||||
*/
|
||||
@@ -494,6 +498,7 @@ function setupEventListeners() {
|
||||
ui.setupDragAndDrop();
|
||||
|
||||
// Debounce timer for live search
|
||||
/** @type {ReturnType<typeof setTimeout>} */
|
||||
let searchDebounceTimer = null;
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
const SEARCH_MIN_CHARS = 3;
|
||||
@@ -726,7 +731,7 @@ export function selectFolder(id, name) {
|
||||
|
||||
/**
|
||||
* Update the storage usage display with the user's actual storage usage
|
||||
* @param {Object} userData - The user data object
|
||||
* @param {User} userData - The user data object
|
||||
*/
|
||||
function updateStorageUsageDisplay(userData) {
|
||||
// Default values
|
||||
|
||||
@@ -136,12 +136,6 @@ export const SECTIONS_MAPPER = {
|
||||
*/
|
||||
function setCurrentSection(section) {
|
||||
if (app.currentSection === section) return false;
|
||||
|
||||
// Set all view flags - true for active section, false for others
|
||||
Object.entries(SECTIONS_MAPPER).forEach(([key, flag]) => {
|
||||
app[flag] = key === section;
|
||||
});
|
||||
|
||||
app.currentSection = section;
|
||||
|
||||
// Update nav item active classes by finding matching item from DOM
|
||||
|
||||
@@ -8,9 +8,14 @@ import { app } from './state.js';
|
||||
import { ui } from './ui.js';
|
||||
|
||||
/**
|
||||
* @param {string} query
|
||||
* @param {string} [sortBy]
|
||||
* @import {SearchCriteria, SortByEnnum} from '../core/types.js'
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} query
|
||||
* @param {SortByEnnum} [sortBy]
|
||||
*/
|
||||
// FIXME: refactor with search.js ?
|
||||
async function performSearch(query, sortBy) {
|
||||
console.log(`Performing search for: "${query}" (sort: ${sortBy || 'relevance'})`);
|
||||
|
||||
@@ -20,9 +25,11 @@ async function performSearch(query, sortBy) {
|
||||
|
||||
ui.showError(`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`);
|
||||
|
||||
/** @type {SearchCriteria} */
|
||||
const options = {
|
||||
recursive: true,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
sort_by: sortBy || 'relevance'
|
||||
};
|
||||
|
||||
@@ -51,7 +58,8 @@ document.addEventListener('search-resort', (e) => {
|
||||
const event = /** @type {CustomEvent<{sort_by: string}>} */ (e);
|
||||
const searchInput = /** @type {HTMLInputElement} */ (document.querySelector('.search-container input'));
|
||||
if (searchInput?.value.trim()) {
|
||||
performSearch(searchInput.value.trim(), event.detail.sort_by);
|
||||
const sortBy = /** @type {SortByEnnum} */ (event.detail.sort_by);
|
||||
performSearch(searchInput.value.trim(), sortBy);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+37
-6
@@ -3,39 +3,70 @@
|
||||
* Centralized mutable state for app and cached DOM references.
|
||||
*/
|
||||
|
||||
/** @import {FolderInfo} from '../core/types.js' */
|
||||
/** @import {FileItem, FolderItem, LightItem} from '../core/types.js' */
|
||||
|
||||
export const app = {
|
||||
currentView: 'grid',
|
||||
|
||||
/** @type {string | null} */
|
||||
currentPath: '',
|
||||
|
||||
/** @type {string | null} */
|
||||
currentFolder: null,
|
||||
|
||||
/** @type {FolderInfo | null} */
|
||||
/** @type {FolderItem | null} */
|
||||
currentFolderInfo: null,
|
||||
|
||||
/** @type {Object | null} */
|
||||
/** @type {FolderItem | null} */
|
||||
contextMenuTargetFolder: null,
|
||||
|
||||
/** @type {Object | null} */
|
||||
/** @type {FileItem | null} */
|
||||
contextMenuTargetFile: null,
|
||||
selectedTargetFolderId: '',
|
||||
moveDialogMode: 'file',
|
||||
|
||||
/** @type {string | null} */
|
||||
moveDialogItemId: null,
|
||||
|
||||
/** @type {'file' | 'folder' | null} */
|
||||
moveDialogItemMode: null,
|
||||
|
||||
/** @type {string | null} */
|
||||
moveDialogCurrentFolderId: null,
|
||||
|
||||
/** @type {Array<{id: string, name: string}>} */
|
||||
moveDialogBreadcrumb: [],
|
||||
|
||||
/** @type {FileItem[] | null} */
|
||||
playlistDialogFiles: null,
|
||||
|
||||
/** @type {String | null} */
|
||||
currentSection: null, // will be defined on first call
|
||||
isSearchMode: false,
|
||||
|
||||
/** @type {FileItem | FolderItem | null} */
|
||||
shareDialogItem: null,
|
||||
|
||||
/** @type {'file' | 'folder' | null} */
|
||||
shareDialogItemType: null,
|
||||
|
||||
/** @type {String | null} */
|
||||
notificationShareUrl: null,
|
||||
|
||||
/** @type {string | null} */
|
||||
userHomeFolderId: null,
|
||||
|
||||
/** @type {string | null} */
|
||||
userHomeFolderName: null,
|
||||
/** @type {Object[]} */
|
||||
|
||||
/** @type {Array<{id: string, name: string}>} */
|
||||
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
|
||||
|
||||
/** @type {LightItem[] | null} */
|
||||
batchMoveItems: null
|
||||
};
|
||||
|
||||
export const appElements = {
|
||||
|
||||
@@ -9,6 +9,11 @@ import { multiSelect } from '../features/files/multiSelect.js';
|
||||
import { appElements } from './state.js';
|
||||
import { ui } from './ui.js';
|
||||
|
||||
/**
|
||||
*
|
||||
* @import {TrashItem} from '../core/types.js'
|
||||
*/
|
||||
|
||||
async function loadTrashItems() {
|
||||
const elements = appElements;
|
||||
|
||||
@@ -25,7 +30,7 @@ async function loadTrashItems() {
|
||||
</div>
|
||||
`;
|
||||
|
||||
ui.updateBreadcrumb('');
|
||||
ui.updateBreadcrumb();
|
||||
|
||||
const trashItems = await fileOps.getTrashItems();
|
||||
|
||||
@@ -46,6 +51,10 @@ async function loadTrashItems() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TrashItem} item
|
||||
*/
|
||||
function addTrashItemToView(item) {
|
||||
const elements = appElements;
|
||||
const isFile = item.item_type === 'file';
|
||||
|
||||
+100
-51
@@ -25,10 +25,17 @@ import { app } from './state.js';
|
||||
import { uiFileTypes } from './uiFileTypes.js';
|
||||
import { uiNotifications } from './uiNotifications.js';
|
||||
|
||||
/**
|
||||
* @import {FileItem, FolderItem} from '../core/types.js'
|
||||
* @import {BatchResult} from '../features/files/fileOperations.js'
|
||||
*/
|
||||
|
||||
// UI Module
|
||||
const ui = {
|
||||
/** @type {HTMLDListElement | null} */
|
||||
//dragPreview,
|
||||
/** @type {HTMLDivElement | null} */
|
||||
dragPreview: null,
|
||||
/** @type {HTMLDivElement | null} */
|
||||
draggedItems: null,
|
||||
|
||||
/**
|
||||
* Initialize context menus and dialogs
|
||||
@@ -338,21 +345,31 @@ const ui = {
|
||||
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DataTransfer} dataTransfer
|
||||
* @returns {Promise<any[]|null>}
|
||||
*/
|
||||
const collectDroppedEntries = async (dataTransfer) => {
|
||||
const items = Array.from(dataTransfer?.items || []);
|
||||
const rootEntries = items.map((it) => (typeof it.webkitGetAsEntry === 'function' ? it.webkitGetAsEntry() : null)).filter(Boolean);
|
||||
|
||||
if (rootEntries.length === 0) return null;
|
||||
|
||||
/** @type {Array<{file: File, relativePath: string}>} */
|
||||
const out = [];
|
||||
|
||||
/**
|
||||
* @param {FileSystemEntry} entry
|
||||
* @param {string} prefix
|
||||
*/
|
||||
const walkEntry = async (entry, prefix = '') => {
|
||||
if (!entry) return;
|
||||
|
||||
if (entry.isFile) {
|
||||
await new Promise((resolve) => {
|
||||
entry.file(
|
||||
(file) => {
|
||||
/** @type {FileSystemFileEntry} */ (entry).file(
|
||||
(/** @type {File} */ file) => {
|
||||
out.push({ file, relativePath: `${prefix}${file.name}` });
|
||||
resolve(undefined);
|
||||
},
|
||||
@@ -364,7 +381,7 @@ const ui = {
|
||||
|
||||
if (entry.isDirectory) {
|
||||
const dirPrefix = `${prefix}${entry.name}/`;
|
||||
const reader = entry.createReader();
|
||||
const reader = /** @type {FileSystemDirectoryEntry} */ (entry).createReader();
|
||||
|
||||
while (true) {
|
||||
const children = await new Promise((resolve) => {
|
||||
@@ -636,7 +653,7 @@ const ui = {
|
||||
|
||||
/**
|
||||
* Check if a file can be previewed in the viewer
|
||||
* @param {Object} file - File object with mime_type property
|
||||
* @param {FileItem} file
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isViewableFile(file) {
|
||||
@@ -647,6 +664,7 @@ const ui = {
|
||||
* Get FontAwesome icon class for a filename based on its extension.
|
||||
* Used as fallback when the backend DTO doesn't include icon_class
|
||||
* (e.g. trash items).
|
||||
* @param {string} fileName
|
||||
*/
|
||||
getIconClass(fileName) {
|
||||
return uiFileTypes.getIconClass(fileName);
|
||||
@@ -655,6 +673,7 @@ const ui = {
|
||||
/**
|
||||
* Get CSS special class for icon styling based on filename extension.
|
||||
* Used as fallback when the backend DTO doesn't include icon_special_class.
|
||||
* @param {string} fileName
|
||||
*/
|
||||
getIconSpecialClass(fileName) {
|
||||
return uiFileTypes.getIconSpecialClass(fileName);
|
||||
@@ -695,13 +714,13 @@ const ui = {
|
||||
* Data store + event delegation (replaces per-item listeners)
|
||||
* ================================================================ */
|
||||
|
||||
/** @type {Map<string, Object>} item data keyed by id */
|
||||
/** @type {Map<string, FolderItem | FileItem>} item data keyed by id */
|
||||
_items: new Map(),
|
||||
|
||||
/** @type {Array<Object>} last rendered folder dataset */
|
||||
/** @type {FolderItem[]} last rendered folder dataset */
|
||||
_lastFolders: [],
|
||||
|
||||
/** @type {Array<Object>} last rendered file dataset */
|
||||
/** @type {FileItem[]} last rendered file dataset */
|
||||
_lastFiles: [],
|
||||
|
||||
/** @type {boolean} */
|
||||
@@ -716,9 +735,7 @@ const ui = {
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object[]} folders
|
||||
* @returns
|
||||
* @param {FolderItem[]} folders
|
||||
*/
|
||||
_renderFoldersToView(folders) {
|
||||
if (!Array.isArray(folders) || folders.length === 0) return;
|
||||
@@ -733,9 +750,7 @@ const ui = {
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object[]} files
|
||||
* @returns
|
||||
* @param {FileItem[]} files
|
||||
*/
|
||||
_renderFilesToView(files) {
|
||||
if (!Array.isArray(files) || files.length === 0) return;
|
||||
@@ -749,6 +764,10 @@ const ui = {
|
||||
target.appendChild(frag);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {any[]} arr
|
||||
* @param {any} item
|
||||
*/
|
||||
_upsertById(arr, item) {
|
||||
if (!Array.isArray(arr) || !item?.id) return;
|
||||
const idx = arr.findIndex((x) => x && x.id === item.id);
|
||||
@@ -796,6 +815,7 @@ const ui = {
|
||||
await fileOps.moveFile(sourceId, targetFolderId);
|
||||
*/
|
||||
|
||||
/** @type {BatchResult} */
|
||||
let result;
|
||||
switch (action) {
|
||||
case 'copy':
|
||||
@@ -843,6 +863,7 @@ const ui = {
|
||||
this._delegationReady = true;
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────
|
||||
/** @param {HTMLDivElement} card */
|
||||
const itemInfo = (card) => {
|
||||
if (!card) return null;
|
||||
const fileId = card.dataset.fileId;
|
||||
@@ -864,6 +885,7 @@ const ui = {
|
||||
return null;
|
||||
};
|
||||
|
||||
/** @param {FileItem} file */
|
||||
const openFile = async (file) => {
|
||||
if (!file) return;
|
||||
if (recent) {
|
||||
@@ -897,6 +919,7 @@ const ui = {
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {HTMLElement} card */
|
||||
const navigateFolder = (card) => {
|
||||
const folderId = card.dataset.folderId;
|
||||
const folderName = card.dataset.folderName;
|
||||
@@ -912,27 +935,31 @@ const ui = {
|
||||
loadFiles();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} card
|
||||
* @param {{ type: string, id: string, name: string | undefined, data: FolderItem | FileItem | undefined }} info
|
||||
*/
|
||||
const setContextTarget = (card, info) => {
|
||||
if (info.type === 'folder') {
|
||||
app.contextMenuTargetFolder = {
|
||||
app.contextMenuTargetFolder = /** @type {FolderItem} */ ({
|
||||
id: info.id,
|
||||
name: card.dataset.folderName,
|
||||
parent_id: card.dataset.parentId || ''
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const fileData = info.data || this._items.get(info.id);
|
||||
app.contextMenuTargetFile = {
|
||||
const fileData = /** @type {FileItem | undefined} */ (info.data || this._items.get(info.id));
|
||||
app.contextMenuTargetFile = /** @type {FileItem} */ ({
|
||||
id: info.id,
|
||||
name: card.dataset.fileName,
|
||||
folder_id: card.dataset.folderId || '',
|
||||
mime_type: fileData?.mime_type || null
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ── click (open / navigate; select only via checkbox) ──
|
||||
filesList.addEventListener('click', (e) => {
|
||||
const card = /** @type {HTMLElement} */ (e.target).closest('.file-item');
|
||||
const card = /** @type {HTMLDivElement | null} */ (/** @type {HTMLElement} */ (e.target).closest('.file-item'));
|
||||
if (!card) return;
|
||||
|
||||
if (/** @type {HTMLElement} */ (e.target).closest('.file-actions')) {
|
||||
@@ -975,7 +1002,7 @@ const ui = {
|
||||
if (info.type === 'folder') {
|
||||
navigateFolder(card);
|
||||
} else {
|
||||
openFile(info.data);
|
||||
openFile(/** @type {FileItem} */ (info.data));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -989,7 +1016,7 @@ const ui = {
|
||||
// ── shared events ──────────────────────
|
||||
|
||||
filesList.addEventListener('contextmenu', (e) => {
|
||||
const card = /** @type {HTMLElement} */ (e.target).closest('.file-item');
|
||||
const card = /** @type {HTMLDivElement | null} */ (/** @type {HTMLElement} */ (e.target).closest('.file-item'));
|
||||
if (!card) return;
|
||||
e.preventDefault();
|
||||
const info = itemInfo(card);
|
||||
@@ -1008,7 +1035,7 @@ const ui = {
|
||||
|
||||
// dragstart
|
||||
filesList.addEventListener('dragstart', (e) => {
|
||||
const card = /** @type {HTMLElement} */ (e.target).closest('.file-item');
|
||||
const card = /** @type {HTMLDivElement | null} */ (/** @type {HTMLElement} */ (e.target).closest('.file-item'));
|
||||
if (!card) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
@@ -1088,9 +1115,11 @@ const ui = {
|
||||
// TODO better naming like ("selection in ${parent.name}") modulo i18n ? ...
|
||||
const now = new Date().toISOString().replace(/T/, ' ').replace(/\.*/, '').replaceAll(/:/g, '-');
|
||||
nameEncoded = `oxicloud ${now}.zip`;
|
||||
/** @type {string[]} */
|
||||
const folders = [];
|
||||
/** @type {string[]} */
|
||||
const files = [];
|
||||
filesList.querySelectorAll(`div.selected`).forEach((e) => {
|
||||
/** @type {NodeListOf<HTMLDivElement>} */ (filesList.querySelectorAll(`div.selected`)).forEach((e) => {
|
||||
const item = itemInfo(e);
|
||||
if (item.type === 'file') {
|
||||
files.push(item.id);
|
||||
@@ -1164,6 +1193,7 @@ const ui = {
|
||||
* Favorite star helper – attaches a direct click handler to a
|
||||
* star <button> so the event never bubbles to the card.
|
||||
* ================================================================ */
|
||||
/** @param {HTMLElement} el */
|
||||
_bindStarClick(el) {
|
||||
const star = el.querySelector('.favorite-star');
|
||||
star?.addEventListener('click', (e) => {
|
||||
@@ -1174,7 +1204,8 @@ const ui = {
|
||||
if (!favorites) return;
|
||||
|
||||
// FIXME: make a function
|
||||
const itemElement = shared?.closest('.file-item');
|
||||
const itemElement = /** @type {HTMLElement | null} */ (shared?.closest('.file-item'));
|
||||
if (!itemElement) return;
|
||||
|
||||
const itemId = itemElement.dataset.fileId ? itemElement.dataset.fileId : itemElement.dataset.folderId;
|
||||
const itemType = itemElement.dataset.fileId ? 'file' : 'folder';
|
||||
@@ -1187,7 +1218,7 @@ const ui = {
|
||||
favorites.removeFromFavorites(itemId, itemType);
|
||||
} else {
|
||||
this.setFavoriteVisualState(itemId, itemType, true);
|
||||
favorites.addToFavorites(itemId, itemName, itemType);
|
||||
favorites.addToFavorites(itemId, itemName, itemType, null);
|
||||
}
|
||||
|
||||
// Keep context-menu label in sync if available
|
||||
@@ -1201,26 +1232,30 @@ const ui = {
|
||||
e.preventDefault();
|
||||
|
||||
// FIXME: make a function
|
||||
const itemElement = shared?.closest('.file-item');
|
||||
const itemElement = /** @type {HTMLElement | null} */ (shared?.closest('.file-item'));
|
||||
if (!itemElement) return;
|
||||
|
||||
const itemId = itemElement.dataset.fileId ? itemElement.dataset.fileId : itemElement.dataset.folderId;
|
||||
const itemType = itemElement.dataset.fileId ? 'file' : 'folder';
|
||||
const itemName = itemElement.dataset.fileId ? itemElement.dataset.fileName : itemElement.dataset.folderName;
|
||||
|
||||
// TODO corrently dirty
|
||||
const item = {
|
||||
const item = /** @type {unknown} */ ({
|
||||
id: itemId,
|
||||
item_id: itemId,
|
||||
item_type: itemType,
|
||||
item_name: itemName
|
||||
};
|
||||
});
|
||||
|
||||
contextMenus.showShareDialog(item, itemType);
|
||||
contextMenus.showShareDialog(/** @type {FileItem} */ (item), itemType);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync favorite visuals for a file/folder across grid and list views.
|
||||
* @param {string} itemId
|
||||
* @param {string} itemType
|
||||
* @param {boolean} isFavorite
|
||||
*/
|
||||
setFavoriteVisualState(itemId, itemType, isFavorite) {
|
||||
const selector = itemType === 'folder' ? `#files-list .file-item[data-folder-id="${itemId}"]` : `#files-list .file-item[data-file-id="${itemId}"]`;
|
||||
@@ -1258,6 +1293,11 @@ const ui = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} itemId
|
||||
* @param {string} itemType
|
||||
* @param {boolean} isShared
|
||||
*/
|
||||
setSharedVisualState(itemId, itemType, isShared) {
|
||||
console.log(`setSharedVisual call for ${itemId} ${itemType} to ${isShared}`);
|
||||
const selector = itemType === 'folder' ? `#files-list .file-item[data-folder-id="${itemId}"]` : `#files-list .file-item[data-file-id="${itemId}"]`;
|
||||
@@ -1273,7 +1313,10 @@ const ui = {
|
||||
* Element-creation helpers
|
||||
* ================================================================ */
|
||||
|
||||
/** Create a list row for a folder */
|
||||
/**
|
||||
* Create a list row for a folder
|
||||
* @param {FolderItem} folder
|
||||
*/
|
||||
_createFolderItem(folder) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'file-item';
|
||||
@@ -1314,7 +1357,10 @@ const ui = {
|
||||
return el;
|
||||
},
|
||||
|
||||
/** Create a grid card for a file */
|
||||
/**
|
||||
* Create a grid card for a file
|
||||
* @param {FileItem} file
|
||||
*/
|
||||
_createFileItem(file) {
|
||||
const iconClass = file.icon_class || this.getIconClass(file.name);
|
||||
const iconSpecialClass = file.icon_special_class || this.getIconSpecialClass(file.name);
|
||||
@@ -1425,7 +1471,7 @@ const ui = {
|
||||
* Render an array of folders into both grid and list views
|
||||
* using DocumentFragment for minimal reflows.
|
||||
*
|
||||
* @param {FolderInfo[]} folders
|
||||
* @param {FolderItem[]} folders
|
||||
*/
|
||||
renderFolders(folders) {
|
||||
if (!this._delegationReady) this.initDelegation();
|
||||
@@ -1442,6 +1488,7 @@ const ui = {
|
||||
/**
|
||||
* Render an array of files into both grid and list views
|
||||
* using DocumentFragment for minimal reflows.
|
||||
* @param {FileItem[]} files
|
||||
*/
|
||||
renderFiles(files) {
|
||||
if (!this._delegationReady) this.initDelegation();
|
||||
@@ -1461,7 +1508,7 @@ const ui = {
|
||||
|
||||
/**
|
||||
* Add a single folder to the active view.
|
||||
* @param {Object} folder - Folder object
|
||||
* @param {FolderItem} folder
|
||||
*/
|
||||
addFolderToView(folder) {
|
||||
if (!this._delegationReady) this.initDelegation();
|
||||
@@ -1479,7 +1526,7 @@ const ui = {
|
||||
|
||||
/**
|
||||
* Add a single file to the active view.
|
||||
* @param {Object} file - File object
|
||||
* @param {FileItem} file
|
||||
*/
|
||||
addFileToView(file) {
|
||||
if (!this._delegationReady) this.initDelegation();
|
||||
@@ -1501,6 +1548,8 @@ const ui = {
|
||||
/**
|
||||
* Toggle selection state of a file/folder card.
|
||||
* Routes through the multiSelect module so batch actions know about selected items.
|
||||
* @param {HTMLDivElement} card
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function toggleCardSelection(card, event) {
|
||||
if (multiSelect) {
|
||||
@@ -1512,6 +1561,8 @@ function toggleCardSelection(card, event) {
|
||||
|
||||
/**
|
||||
* Show the context menu anchored next to a trigger element (the 3-dot button).
|
||||
* @param {HTMLElement} triggerElement
|
||||
* @param {string} menuId
|
||||
*/
|
||||
function showContextMenuAtElement(triggerElement, menuId) {
|
||||
// Hide any open menus first
|
||||
@@ -1542,8 +1593,6 @@ function showContextMenuAtElement(triggerElement, menuId) {
|
||||
menu.classList.remove('hidden');
|
||||
}
|
||||
|
||||
let __rubberBandJustFinished = false;
|
||||
|
||||
/**
|
||||
* Rubber band (lasso) selection — click + drag on empty grid area
|
||||
* to draw a rectangle and select all cards it touches.
|
||||
@@ -1567,16 +1616,18 @@ function initRubberBandSelection() {
|
||||
if (!container) return;
|
||||
|
||||
container.addEventListener('mousedown', (e) => {
|
||||
if (!(e instanceof MouseEvent)) return;
|
||||
// Only start if clicking empty area (not on a card, button, menu, input…)
|
||||
if (e.button !== 0) return; // left click only
|
||||
const target = /** @type {Element} */ (e.target);
|
||||
if (
|
||||
e.target.closest('.file-item') ||
|
||||
e.target.closest('.context-menu') ||
|
||||
e.target.closest('.upload-dropdown') ||
|
||||
e.target.closest('button') ||
|
||||
e.target.closest('input') ||
|
||||
e.target.closest('.breadcrumb') ||
|
||||
e.target.closest('.list-header')
|
||||
target.closest('.file-item') ||
|
||||
target.closest('.context-menu') ||
|
||||
target.closest('.upload-dropdown') ||
|
||||
target.closest('button') ||
|
||||
target.closest('input') ||
|
||||
target.closest('.breadcrumb') ||
|
||||
target.closest('.list-header')
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -1627,14 +1678,14 @@ function initRubberBandSelection() {
|
||||
|
||||
// Sync with multiSelect module
|
||||
if (multiSelect) {
|
||||
const info = multiSelect._extractInfo(card);
|
||||
const info = multiSelect._extractInfo(/** @type {HTMLDivElement} */ (card));
|
||||
if (info) multiSelect.select(info.id, info.name, info.type, info.parentId);
|
||||
}
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
// Deselect from multiSelect module
|
||||
if (multiSelect) {
|
||||
const info = multiSelect._extractInfo(card);
|
||||
const info = multiSelect._extractInfo(/** @type {HTMLDivElement} */ (card));
|
||||
if (info) multiSelect.deselect(info.id);
|
||||
}
|
||||
}
|
||||
@@ -1651,10 +1702,7 @@ function initRubberBandSelection() {
|
||||
// Suppress the click event that follows mouseup so the global
|
||||
// deselect handler doesn't immediately clear the selection.
|
||||
if (hadSelection) {
|
||||
__rubberBandJustFinished = true;
|
||||
requestAnimationFrame(() => {
|
||||
__rubberBandJustFinished = false;
|
||||
});
|
||||
requestAnimationFrame(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1709,6 +1757,7 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t
|
||||
overlay.classList.add('active');
|
||||
});
|
||||
|
||||
/** @param {boolean} result */
|
||||
const cleanup = (result) => {
|
||||
overlay.classList.remove('active');
|
||||
setTimeout(() => overlay.remove(), 200);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { isTextViewable } from '../core/formatters.js';
|
||||
|
||||
/** @import {FileInfo} from '../core/types.js' */
|
||||
/** @import {FileItem} from '../core/types.js' */
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const ICON_CLASS_MAP = {
|
||||
@@ -158,7 +158,7 @@ const uiFileTypes = {
|
||||
// TODO: 'd better to use a canViw() method in inlineViewer
|
||||
/**
|
||||
*
|
||||
* @param {FileInfo} file
|
||||
* @param {FileItem} file
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isViewableFile(file) {
|
||||
|
||||
Reference in New Issue
Block a user