style: apply linter suggestions
This commit is contained in:
@@ -115,7 +115,7 @@ async function checkAuthentication() {
|
|||||||
window.location.href = '/login?source=session_expired';
|
window.location.href = '/login?source=session_expired';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
localStorage.removeItem(USER_DATA_KEY);
|
localStorage.removeItem(USER_DATA_KEY);
|
||||||
window.location.href = '/login?source=session_expired';
|
window.location.href = '/login?source=session_expired';
|
||||||
return;
|
return;
|
||||||
@@ -128,9 +128,11 @@ async function checkAuthentication() {
|
|||||||
console.log('No cached user data, fetching from server');
|
console.log('No cached user data, fetching from server');
|
||||||
try {
|
try {
|
||||||
const freshData = await refreshUserData();
|
const freshData = await refreshUserData();
|
||||||
if (freshData && freshData.username) {
|
if (freshData?.username) {
|
||||||
const userInitials = freshData.username.substring(0, 2).toUpperCase();
|
const userInitials = freshData.username.substring(0, 2).toUpperCase();
|
||||||
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach((el) => (el.textContent = userInitials));
|
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach((el) => {
|
||||||
|
el.textContent = userInitials;
|
||||||
|
});
|
||||||
window.updateStorageUsageDisplay(freshData);
|
window.updateStorageUsageDisplay(freshData);
|
||||||
resolveHomeFolder().then(() => window.loadFiles());
|
resolveHomeFolder().then(() => window.loadFiles());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ async function getFolder(id) {
|
|||||||
cache: 'no-store'
|
cache: 'no-store'
|
||||||
};
|
};
|
||||||
|
|
||||||
let folderInformations = await fetch(`/api/folders/${id}`, requestOptions);
|
const folderInformations = await fetch(`/api/folders/${id}`, requestOptions);
|
||||||
if (folderInformations.ok) {
|
if (folderInformations.ok) {
|
||||||
return folderInformations.json();
|
return folderInformations.json();
|
||||||
} else {
|
} else {
|
||||||
@@ -67,7 +67,7 @@ async function rebuildBreadCrumb() {
|
|||||||
while (id !== null) {
|
while (id !== null) {
|
||||||
console.log(`fetching folder information for folder ${id}`);
|
console.log(`fetching folder information for folder ${id}`);
|
||||||
try {
|
try {
|
||||||
let folderInfo = await getFolder(id);
|
const folderInfo = await getFolder(id);
|
||||||
|
|
||||||
// store the Leaf which is the current folder
|
// store the Leaf which is the current folder
|
||||||
if (currentFolderInfo === null) {
|
if (currentFolderInfo === null) {
|
||||||
@@ -84,7 +84,7 @@ async function rebuildBreadCrumb() {
|
|||||||
|
|
||||||
// iterate to parent folder
|
// iterate to parent folder
|
||||||
id = folderInfo.parent_id;
|
id = folderInfo.parent_id;
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
console.log(`Error loading information from folder ${app.currentPath}, falling back to ${app.userHomeFolderId}`);
|
console.log(`Error loading information from folder ${app.currentPath}, falling back to ${app.userHomeFolderId}`);
|
||||||
// fallback of root
|
// fallback of root
|
||||||
window.uiNotifications.show(
|
window.uiNotifications.show(
|
||||||
@@ -110,7 +110,6 @@ async function rebuildBreadCrumb() {
|
|||||||
*/
|
*/
|
||||||
async function loadFiles(options = { insertHistory: true }) {
|
async function loadFiles(options = { insertHistory: true }) {
|
||||||
const app = window.app;
|
const app = window.app;
|
||||||
const elements = window.appElements;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Starting loadFiles() - loading files...', options);
|
console.log('Starting loadFiles() - loading files...', options);
|
||||||
@@ -125,7 +124,7 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
window.isLoadingFiles = true;
|
window.isLoadingFiles = true;
|
||||||
|
|
||||||
// This to avoid blinking page, a better solution would be to put loading on an overlay and remove timeout
|
// This to avoid blinking page, a better solution would be to put loading on an overlay and remove timeout
|
||||||
let loadingFiles = setTimeout(() => {
|
const loadingFiles = setTimeout(() => {
|
||||||
// display loader after few delay (will be canceled if result take less time)
|
// display loader after few delay (will be canceled if result take less time)
|
||||||
window.ui.showError(`
|
window.ui.showError(`
|
||||||
<div class="files-loading-spinner">
|
<div class="files-loading-spinner">
|
||||||
@@ -139,7 +138,7 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
await window.resolveHomeFolder();
|
await window.resolveHomeFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = new Date().getTime();
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
await rebuildBreadCrumb();
|
await rebuildBreadCrumb();
|
||||||
|
|
||||||
@@ -181,7 +180,6 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
|
|
||||||
if (forceRefresh) {
|
if (forceRefresh) {
|
||||||
url += `&force_refresh=true`;
|
url += `&force_refresh=true`;
|
||||||
// @ts-ignore
|
|
||||||
requestOptions.headers['X-Force-Refresh'] = 'true';
|
requestOptions.headers['X-Force-Refresh'] = 'true';
|
||||||
console.log('Forcing complete refresh ignoring cache');
|
console.log('Forcing complete refresh ignoring cache');
|
||||||
}
|
}
|
||||||
@@ -226,7 +224,7 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
let fileFound = null;
|
let fileFound = null;
|
||||||
|
|
||||||
// lookup for the given fle
|
// lookup for the given fle
|
||||||
for( const file of fileList) {
|
for (const file of fileList) {
|
||||||
if (file.id === window.app.viewFile) {
|
if (file.id === window.app.viewFile) {
|
||||||
fileFound = file;
|
fileFound = file;
|
||||||
break;
|
break;
|
||||||
@@ -236,14 +234,13 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
if (fileFound) {
|
if (fileFound) {
|
||||||
console.log(`file ${window.app.viewFile} found, calling viewer`);
|
console.log(`file ${window.app.viewFile} found, calling viewer`);
|
||||||
await window.inlineViewer.openFile(fileFound);
|
await window.inlineViewer.openFile(fileFound);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// remove file
|
// remove file
|
||||||
console.log(`file ${window.app.viewFile} not found`);
|
console.log(`file ${window.app.viewFile} not found`);
|
||||||
window.app.viewFile = null;
|
window.app.viewFile = null;
|
||||||
|
|
||||||
// correct url/history as file is not found
|
// correct url/history as file is not found
|
||||||
window.updateHistory( false);
|
window.updateHistory(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-20
@@ -126,7 +126,7 @@ function setActionsBarMode(mode, force = false) {
|
|||||||
elements.gridViewBtn = document.getElementById('grid-view-btn');
|
elements.gridViewBtn = document.getElementById('grid-view-btn');
|
||||||
elements.listViewBtn = document.getElementById('list-view-btn');
|
elements.listViewBtn = document.getElementById('list-view-btn');
|
||||||
|
|
||||||
if (window.i18n && window.i18n.translateElement) {
|
if (window.i18n?.translateElement) {
|
||||||
window.i18n.translateElement(elements.actionsBar);
|
window.i18n.translateElement(elements.actionsBar);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,8 +231,8 @@ function deserializeHash() {
|
|||||||
|
|
||||||
if (hash_elements[1] === 'files' && hash_elements[2] === 'folder' && hash_elements[3] !== null) {
|
if (hash_elements[1] === 'files' && hash_elements[2] === 'folder' && hash_elements[3] !== null) {
|
||||||
hashContext.path = hash_elements[3];
|
hashContext.path = hash_elements[3];
|
||||||
|
|
||||||
if (hash_elements[4] == 'file' && hash_elements[5] !== null) {
|
if (hash_elements[4] === 'file' && hash_elements[5] !== null) {
|
||||||
hashContext.file = hash_elements[5];
|
hashContext.file = hash_elements[5];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,7 +248,7 @@ function deserializeHash() {
|
|||||||
function updateHistory(insertHistory) {
|
function updateHistory(insertHistory) {
|
||||||
const app = window.app;
|
const app = window.app;
|
||||||
|
|
||||||
let historyData = {
|
const historyData = {
|
||||||
section: app.currentSection,
|
section: app.currentSection,
|
||||||
id: app.currentFolder,
|
id: app.currentFolder,
|
||||||
file: app.viewFile
|
file: app.viewFile
|
||||||
@@ -261,7 +261,7 @@ function updateHistory(insertHistory) {
|
|||||||
historyUrl = historyUrl.concat('/folder/', app.currentFolderInfo.id);
|
historyUrl = historyUrl.concat('/folder/', app.currentFolderInfo.id);
|
||||||
|
|
||||||
if (window.app.viewFile) {
|
if (window.app.viewFile) {
|
||||||
historyUrl = historyUrl.concat('/file/', window.app.viewFile);
|
historyUrl = historyUrl.concat('/file/', window.app.viewFile);
|
||||||
}
|
}
|
||||||
// update title
|
// update title
|
||||||
document.title = `OxiCloud: ${app.currentFolderInfo.path}`;
|
document.title = `OxiCloud: ${app.currentFolderInfo.path}`;
|
||||||
@@ -325,7 +325,7 @@ function initApp() {
|
|||||||
cacheElements();
|
cacheElements();
|
||||||
|
|
||||||
// Initialize file sharing module first
|
// Initialize file sharing module first
|
||||||
if (window.fileSharing && window.fileSharing.init) {
|
if (window.fileSharing?.init) {
|
||||||
window.fileSharing.init();
|
window.fileSharing.init();
|
||||||
} else {
|
} else {
|
||||||
console.warn('fileSharing module not fully initialized');
|
console.warn('fileSharing module not fully initialized');
|
||||||
@@ -349,7 +349,7 @@ function initApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize favorites module if available
|
// Initialize favorites module if available
|
||||||
if (window.favorites && window.favorites.init) {
|
if (window.favorites?.init) {
|
||||||
console.log('Initializing favorites module');
|
console.log('Initializing favorites module');
|
||||||
window.favorites.init();
|
window.favorites.init();
|
||||||
} else {
|
} else {
|
||||||
@@ -357,7 +357,7 @@ function initApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize recent files module if available
|
// Initialize recent files module if available
|
||||||
if (window.recent && window.recent.init) {
|
if (window.recent?.init) {
|
||||||
console.log('Initializing recent files module');
|
console.log('Initializing recent files module');
|
||||||
window.recent.init();
|
window.recent.init();
|
||||||
} else {
|
} else {
|
||||||
@@ -365,21 +365,21 @@ function initApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize multi-select / batch actions
|
// Initialize multi-select / batch actions
|
||||||
if (window.multiSelect && window.multiSelect.init) {
|
if (window.multiSelect?.init) {
|
||||||
console.log('Initializing multi-select module');
|
console.log('Initializing multi-select module');
|
||||||
window.multiSelect.init();
|
window.multiSelect.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('authenticationDone', () => {
|
window.addEventListener('authenticationDone', () => {
|
||||||
// Check if a context was provided in the URL
|
// Check if a context was provided in the URL
|
||||||
let hashContext = deserializeHash();
|
const hashContext = deserializeHash();
|
||||||
switchSectionTo(hashContext.section);
|
switchSectionTo(hashContext.section);
|
||||||
if (hashContext.section === 'files') {
|
if (hashContext.section === 'files') {
|
||||||
if (hashContext.path) {
|
if (hashContext.path) {
|
||||||
console.log(`init: reusing folder from hash URL: ${hashContext.path}`);
|
console.log(`init: reusing folder from hash URL: ${hashContext.path}`);
|
||||||
window.app.currentPath = hashContext.path;
|
window.app.currentPath = hashContext.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hashContext.file !== null) {
|
if (hashContext.file !== null) {
|
||||||
window.app.viewFile = hashContext.file;
|
window.app.viewFile = hashContext.file;
|
||||||
}
|
}
|
||||||
@@ -388,7 +388,7 @@ function initApp() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Wait for translations to load before checking authentication
|
// Wait for translations to load before checking authentication
|
||||||
if (window.i18n && window.i18n.isLoaded && window.i18n.isLoaded()) {
|
if (window.i18n?.isLoaded?.()) {
|
||||||
// Translations already loaded, proceed with authentication
|
// Translations already loaded, proceed with authentication
|
||||||
window.checkAuthentication();
|
window.checkAuthentication();
|
||||||
} else {
|
} else {
|
||||||
@@ -401,7 +401,7 @@ function initApp() {
|
|||||||
|
|
||||||
// Set a timeout as a fallback in case translations take too long
|
// Set a timeout as a fallback in case translations take too long
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!window.i18n || !window.i18n.isLoaded || !window.i18n.isLoaded()) {
|
if (!window.i18n?.isLoaded?.()) {
|
||||||
console.warn('Translations loading timeout, proceeding with authentication anyway');
|
console.warn('Translations loading timeout, proceeding with authentication anyway');
|
||||||
window.checkAuthentication();
|
window.checkAuthentication();
|
||||||
}
|
}
|
||||||
@@ -451,7 +451,9 @@ function setupUploadDropdown() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const isOpen = menu.classList.contains('show');
|
const isOpen = menu.classList.contains('show');
|
||||||
// Close any other open dropdowns
|
// Close any other open dropdowns
|
||||||
document.querySelectorAll('.upload-dropdown-menu.show').forEach((m) => m.classList.remove('show'));
|
document.querySelectorAll('.upload-dropdown-menu.show').forEach((m) => {
|
||||||
|
m.classList.remove('show');
|
||||||
|
});
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
menu.classList.add('show');
|
menu.classList.add('show');
|
||||||
}
|
}
|
||||||
@@ -462,14 +464,14 @@ function setupUploadDropdown() {
|
|||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
// remove+add stable handler: guarantees exactly one global listener
|
// remove+add stable handler: guarantees exactly one global listener
|
||||||
if (uploadDropdownDocumentClickHandler) {
|
if (uploadDropdownDocumentClickHandler) {
|
||||||
// @ts-ignore
|
|
||||||
document.removeEventListener('click', uploadDropdownDocumentClickHandler);
|
document.removeEventListener('click', uploadDropdownDocumentClickHandler);
|
||||||
}
|
}
|
||||||
uploadDropdownDocumentClickHandler = (e) => {
|
uploadDropdownDocumentClickHandler = (e) => {
|
||||||
if (e.target.closest('#upload-dropdown')) return;
|
if (e.target.closest('#upload-dropdown')) return;
|
||||||
document.querySelectorAll('.upload-dropdown-menu.show').forEach((m) => m.classList.remove('show'));
|
document.querySelectorAll('.upload-dropdown-menu.show').forEach((m) => {
|
||||||
|
m.classList.remove('show');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
// @ts-ignore
|
|
||||||
document.addEventListener('click', uploadDropdownDocumentClickHandler);
|
document.addEventListener('click', uploadDropdownDocumentClickHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,13 +589,15 @@ function setupEventListeners() {
|
|||||||
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) => navItem.classList.remove('active'));
|
elements.navItems.forEach((navItem) => {
|
||||||
|
navItem.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
// Add active class to clicked item
|
// Add active class to clicked item
|
||||||
item.classList.add('active');
|
item.classList.add('active');
|
||||||
let _updateHistory = true;
|
let _updateHistory = true;
|
||||||
|
|
||||||
let 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':
|
||||||
// Switch to shared view
|
// Switch to shared view
|
||||||
@@ -712,7 +716,7 @@ function updateStorageUsageDisplay(userData) {
|
|||||||
storageInfo.removeAttribute('data-i18n');
|
storageInfo.removeAttribute('data-i18n');
|
||||||
|
|
||||||
// Use i18n if available
|
// Use i18n if available
|
||||||
if (window.i18n && window.i18n.t) {
|
if (window.i18n?.t) {
|
||||||
storageInfo.textContent = window.i18n.t('storage.used', {
|
storageInfo.textContent = window.i18n.t('storage.used', {
|
||||||
percentage: usagePercentage,
|
percentage: usagePercentage,
|
||||||
used: usedFormatted,
|
used: usedFormatted,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ function getSectionFromNavItem(navItem) {
|
|||||||
* @returns {boolean} true if the section changed
|
* @returns {boolean} true if the section changed
|
||||||
*/
|
*/
|
||||||
function setCurrentSection(section) {
|
function setCurrentSection(section) {
|
||||||
if (window.app.currentSection == section) return false;
|
if (window.app.currentSection === section) return false;
|
||||||
|
|
||||||
// Set all view flags - true for active section, false for others
|
// Set all view flags - true for active section, false for others
|
||||||
Object.entries(VIEW_FLAGS).forEach(([key, flag]) => {
|
Object.entries(VIEW_FLAGS).forEach(([key, flag]) => {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ async function performSearch(query, sortBy) {
|
|||||||
|
|
||||||
document.addEventListener('search-resort', (e) => {
|
document.addEventListener('search-resort', (e) => {
|
||||||
const searchInput = document.querySelector('.search-container input');
|
const searchInput = document.querySelector('.search-container input');
|
||||||
if (searchInput && searchInput.value.trim()) {
|
if (searchInput?.value.trim()) {
|
||||||
performSearch(searchInput.value.trim(), e.detail.sort_by);
|
performSearch(searchInput.value.trim(), e.detail.sort_by);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ async function loadTrashItems() {
|
|||||||
try {
|
try {
|
||||||
if (window.multiSelect) window.multiSelect.clear();
|
if (window.multiSelect) window.multiSelect.clear();
|
||||||
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
||||||
const _tt = window.i18n && window.i18n.t ? window.i18n.t : (k) => k.split('.').pop();
|
const _tt = window.i18n?.t ? window.i18n.t : (k) => k.split('.').pop();
|
||||||
elements.filesList.innerHTML = `
|
elements.filesList.innerHTML = `
|
||||||
<div class="list-header trash-header">
|
<div class="list-header trash-header">
|
||||||
<div data-i18n="files.name">${_tt('files.name')}</div>
|
<div data-i18n="files.name">${_tt('files.name')}</div>
|
||||||
@@ -53,8 +53,8 @@ function addTrashItemToView(item) {
|
|||||||
iconClass = item.icon_class || 'fas fa-folder';
|
iconClass = item.icon_class || 'fas fa-folder';
|
||||||
typeLabel = window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder';
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder';
|
||||||
} else {
|
} else {
|
||||||
iconClass = item.icon_class || (window.ui && window.ui.getIconClass ? window.ui.getIconClass(item.name) : 'fas fa-file');
|
iconClass = item.icon_class || (window.ui?.getIconClass ? window.ui.getIconClass(item.name) : 'fas fa-file');
|
||||||
iconSpecialClass = window.ui && window.ui.getIconSpecialClass ? window.ui.getIconSpecialClass(item.name) : '';
|
iconSpecialClass = window.ui?.getIconSpecialClass ? window.ui.getIconSpecialClass(item.name) : '';
|
||||||
const cat = item.category || '';
|
const cat = item.category || '';
|
||||||
typeLabel = cat
|
typeLabel = cat
|
||||||
? window.i18n
|
? window.i18n
|
||||||
|
|||||||
+54
-56
@@ -359,7 +359,7 @@ const ui = {
|
|||||||
// 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);
|
||||||
if (droppedEntries && droppedEntries.length > 0) {
|
if (droppedEntries && droppedEntries.length > 0) {
|
||||||
const hasFolderStructure = droppedEntries.some((x) => x.relativePath && x.relativePath.includes('/'));
|
const hasFolderStructure = droppedEntries.some((x) => x.relativePath?.includes('/'));
|
||||||
if (hasFolderStructure) {
|
if (hasFolderStructure) {
|
||||||
fileOps.uploadFolderEntries(droppedEntries);
|
fileOps.uploadFolderEntries(droppedEntries);
|
||||||
} else {
|
} else {
|
||||||
@@ -372,7 +372,7 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect folder drops: files from folder drops have webkitRelativePath set
|
// Detect folder drops: files from folder drops have webkitRelativePath set
|
||||||
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath && f.webkitRelativePath.includes('/'));
|
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath?.includes('/'));
|
||||||
if (hasRelativePaths) {
|
if (hasRelativePaths) {
|
||||||
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
||||||
} else {
|
} else {
|
||||||
@@ -415,7 +415,7 @@ const ui = {
|
|||||||
// 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);
|
||||||
if (droppedEntries && droppedEntries.length > 0) {
|
if (droppedEntries && droppedEntries.length > 0) {
|
||||||
const hasFolderStructure = droppedEntries.some((x) => x.relativePath && x.relativePath.includes('/'));
|
const hasFolderStructure = droppedEntries.some((x) => x.relativePath?.includes('/'));
|
||||||
if (hasFolderStructure) {
|
if (hasFolderStructure) {
|
||||||
fileOps.uploadFolderEntries(droppedEntries);
|
fileOps.uploadFolderEntries(droppedEntries);
|
||||||
} else {
|
} else {
|
||||||
@@ -428,7 +428,7 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect folder drops: files from folder drops have webkitRelativePath set
|
// Detect folder drops: files from folder drops have webkitRelativePath set
|
||||||
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath && f.webkitRelativePath.includes('/'));
|
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath?.includes('/'));
|
||||||
if (hasRelativePaths) {
|
if (hasRelativePaths) {
|
||||||
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
||||||
} else {
|
} else {
|
||||||
@@ -474,13 +474,11 @@ const ui = {
|
|||||||
updateBreadcrumb() {
|
updateBreadcrumb() {
|
||||||
const breadcrumb = document.querySelector('.breadcrumb');
|
const breadcrumb = document.querySelector('.breadcrumb');
|
||||||
breadcrumb.innerHTML = '';
|
breadcrumb.innerHTML = '';
|
||||||
|
|
||||||
const self = this;
|
|
||||||
const path = window.app.breadcrumbPath; // [{id, name}, ...]
|
const path = window.app.breadcrumbPath; // [{id, name}, ...]
|
||||||
|
|
||||||
// Helper function to safely get translation text
|
// Helper function to safely get translation text
|
||||||
const getTranslatedText = (key, defaultValue) => {
|
const getTranslatedText = (key, defaultValue) => {
|
||||||
if (!window.i18n || !window.i18n.t) return defaultValue;
|
if (!window.i18n?.t) return defaultValue;
|
||||||
return window.i18n.t(key);
|
return window.i18n.t(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -496,7 +494,7 @@ const ui = {
|
|||||||
homeIcon.addEventListener('click', () => {
|
homeIcon.addEventListener('click', () => {
|
||||||
window.app.breadcrumbPath = [];
|
window.app.breadcrumbPath = [];
|
||||||
window.app.currentPath = window.app.userHomeFolderId;
|
window.app.currentPath = window.app.userHomeFolderId;
|
||||||
self.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -535,7 +533,7 @@ const ui = {
|
|||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
window.app.breadcrumbPath = path.slice(0, index + 1);
|
window.app.breadcrumbPath = path.slice(0, index + 1);
|
||||||
window.app.currentPath = segment.id;
|
window.app.currentPath = segment.id;
|
||||||
self.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -543,7 +541,7 @@ const ui = {
|
|||||||
// 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 = e.target.closest('span');
|
||||||
if (!card || !card.dataset.folderId) return;
|
if (!card?.dataset.folderId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
card.classList.add('drop-target');
|
card.classList.add('drop-target');
|
||||||
});
|
});
|
||||||
@@ -552,7 +550,7 @@ const ui = {
|
|||||||
item.addEventListener('dragleave', (e) => {
|
item.addEventListener('dragleave', (e) => {
|
||||||
console.log('dragleave ', e);
|
console.log('dragleave ', e);
|
||||||
const card = e.target.closest('span');
|
const card = e.target.closest('span');
|
||||||
if (!card || !card.dataset.folderId) return;
|
if (!card?.dataset.folderId) return;
|
||||||
card.classList.remove('drop-target');
|
card.classList.remove('drop-target');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -567,7 +565,7 @@ const ui = {
|
|||||||
card.classList.remove('drop-target');
|
card.classList.remove('drop-target');
|
||||||
|
|
||||||
const action = e.dataTransfer?.dropEffect;
|
const action = e.dataTransfer?.dropEffect;
|
||||||
await self._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
|
||||||
@@ -693,7 +691,7 @@ const ui = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_upsertById(arr, item) {
|
_upsertById(arr, item) {
|
||||||
if (!Array.isArray(arr) || !item || !item.id) return;
|
if (!Array.isArray(arr) || !item?.id) return;
|
||||||
const idx = arr.findIndex((x) => x && x.id === item.id);
|
const idx = arr.findIndex((x) => x && x.id === item.id);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
arr[idx] = item;
|
arr[idx] = item;
|
||||||
@@ -709,11 +707,11 @@ const ui = {
|
|||||||
* @param {any} dataTransfer fallback if nothing is selected
|
* @param {any} dataTransfer fallback if nothing is selected
|
||||||
*/
|
*/
|
||||||
async _dropToFolder(action, targetFolderId, dataTransfer) {
|
async _dropToFolder(action, targetFolderId, dataTransfer) {
|
||||||
let selection = window.multiSelect.getSelection(targetFolderId);
|
const selection = window.multiSelect.getSelection(targetFolderId);
|
||||||
|
|
||||||
window.multiSelect.clear();
|
window.multiSelect.clear();
|
||||||
|
|
||||||
if (selection.fileIds.length == 0 && selection.folderIds.length == 0) {
|
if (selection.fileIds.length === 0 && selection.folderIds.length === 0) {
|
||||||
// try to use dataTransfer (direct move without selection)
|
// try to use dataTransfer (direct move without selection)
|
||||||
const id = dataTransfer.getData('text/plain');
|
const id = dataTransfer.getData('text/plain');
|
||||||
const isFolder = dataTransfer.getData('application/oxicloud-folder') === 'true';
|
const isFolder = dataTransfer.getData('application/oxicloud-folder') === 'true';
|
||||||
@@ -785,8 +783,6 @@ const ui = {
|
|||||||
if (!filesList) return;
|
if (!filesList) return;
|
||||||
this._delegationReady = true;
|
this._delegationReady = true;
|
||||||
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
// ── helpers ────────────────────────────────────────────────
|
// ── helpers ────────────────────────────────────────────────
|
||||||
const itemInfo = (card) => {
|
const itemInfo = (card) => {
|
||||||
if (!card) return null;
|
if (!card) return null;
|
||||||
@@ -796,7 +792,7 @@ const ui = {
|
|||||||
type: 'file',
|
type: 'file',
|
||||||
id: fileId,
|
id: fileId,
|
||||||
name: card.dataset.fileName,
|
name: card.dataset.fileName,
|
||||||
data: self._items.get(fileId)
|
data: this._items.get(fileId)
|
||||||
};
|
};
|
||||||
const folderId = card.dataset.folderId;
|
const folderId = card.dataset.folderId;
|
||||||
if (folderId)
|
if (folderId)
|
||||||
@@ -804,7 +800,7 @@ const ui = {
|
|||||||
type: 'folder',
|
type: 'folder',
|
||||||
id: folderId,
|
id: folderId,
|
||||||
name: card.dataset.folderName,
|
name: card.dataset.folderName,
|
||||||
data: self._items.get(folderId)
|
data: this._items.get(folderId)
|
||||||
};
|
};
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -818,25 +814,23 @@ const ui = {
|
|||||||
// But NOT image files - those should be previewed in the inline viewer
|
// But NOT image files - those should be previewed in the inline viewer
|
||||||
const ext = (file.name || '').split('.').pop().toLowerCase();
|
const ext = (file.name || '').split('.').pop().toLowerCase();
|
||||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'heic', 'heif', 'avif', 'tiff'];
|
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'heic', 'heif', 'avif', 'tiff'];
|
||||||
const isImage = (file.mime_type && file.mime_type.startsWith('image/')) || imageExts.includes(ext);
|
const isImage = file.mime_type?.startsWith('image/') || imageExts.includes(ext);
|
||||||
try {
|
try {
|
||||||
if (!isImage && window.wopiEditor && await window.wopiEditor.canEdit(file.name)) {
|
if (!isImage && window.wopiEditor && (await window.wopiEditor.canEdit(file.name))) {
|
||||||
await window.wopiEditor.openInModal(file.id, file.name, 'edit')
|
await window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
console.warn(`WOPI Editor failed, falling bck to classic view `, e);
|
console.warn(`WOPI Editor failed, falling bck to classic view `, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.isViewableFile(file) || isImage) {
|
if (this.isViewableFile(file) || isImage) {
|
||||||
if (window.inlineViewer) {
|
if (window.inlineViewer) {
|
||||||
window.inlineViewer.openFile(file);
|
window.inlineViewer.openFile(file);
|
||||||
// update history
|
// update history
|
||||||
window.app.viewFile = file.id;
|
window.app.viewFile = file.id;
|
||||||
window.updateHistory(false);
|
window.updateHistory(false);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
window.fileOps.downloadFile(file.id, file.name);
|
window.fileOps.downloadFile(file.id, file.name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -849,7 +843,7 @@ const ui = {
|
|||||||
const folderName = card.dataset.folderName;
|
const folderName = card.dataset.folderName;
|
||||||
window.app.breadcrumbPath.push({ id: folderId, name: folderName });
|
window.app.breadcrumbPath.push({ id: folderId, name: folderName });
|
||||||
window.app.currentPath = folderId;
|
window.app.currentPath = folderId;
|
||||||
self.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
window.loadFiles();
|
window.loadFiles();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -940,7 +934,7 @@ const ui = {
|
|||||||
window.contextMenus.syncFavoriteOptionLabels();
|
window.contextMenus.syncFavoriteOptionLabels();
|
||||||
}
|
}
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
||||||
window.contextMenus.syncWopiOptionVisibility().catch(function () {});
|
window.contextMenus.syncWopiOptionVisibility().catch(() => {});
|
||||||
}
|
}
|
||||||
menu.style.left = `${e.pageX}px`;
|
menu.style.left = `${e.pageX}px`;
|
||||||
menu.style.top = `${e.pageY}px`;
|
menu.style.top = `${e.pageY}px`;
|
||||||
@@ -949,7 +943,7 @@ const ui = {
|
|||||||
|
|
||||||
// dragstart
|
// dragstart
|
||||||
filesList.addEventListener('dragstart', (e) => {
|
filesList.addEventListener('dragstart', (e) => {
|
||||||
let card = e.target.closest('.file-item');
|
const card = e.target.closest('.file-item');
|
||||||
if (!card) {
|
if (!card) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -968,11 +962,11 @@ const ui = {
|
|||||||
// allow copy or move (handled by the browser)
|
// allow copy or move (handled by the browser)
|
||||||
e.dataTransfer.effectAllowed = 'copyMove';
|
e.dataTransfer.effectAllowed = 'copyMove';
|
||||||
|
|
||||||
self.draggedItems = document.createElement('div');
|
this.draggedItems = document.createElement('div');
|
||||||
self.draggedItems.className = 'dragged-items';
|
this.draggedItems.className = 'dragged-items';
|
||||||
|
|
||||||
let selectedCardFromList = filesList.querySelectorAll(`div.selected > div.name-cell`);
|
let selectedCardFromList = filesList.querySelectorAll(`div.selected > div.name-cell`);
|
||||||
if (selectedCardFromList.length == 0) {
|
if (selectedCardFromList.length === 0) {
|
||||||
// fallback to current element
|
// fallback to current element
|
||||||
selectedCardFromList = card.querySelectorAll('div.name-cell');
|
selectedCardFromList = card.querySelectorAll('div.name-cell');
|
||||||
}
|
}
|
||||||
@@ -982,8 +976,8 @@ const ui = {
|
|||||||
let lastItemDiv = null;
|
let lastItemDiv = null;
|
||||||
|
|
||||||
while (index < selectedCardFromList.length && index < maxElements) {
|
while (index < selectedCardFromList.length && index < maxElements) {
|
||||||
let iconCell = document.createElement('div');
|
const iconCell = document.createElement('div');
|
||||||
let icon = selectedCardFromList[index].getElementsByClassName('file-icon').item(0)?.cloneNode(true);
|
const icon = selectedCardFromList[index].getElementsByClassName('file-icon').item(0)?.cloneNode(true);
|
||||||
if (icon) {
|
if (icon) {
|
||||||
iconCell.appendChild(icon);
|
iconCell.appendChild(icon);
|
||||||
iconCell.querySelectorAll('img')?.forEach((img) => {
|
iconCell.querySelectorAll('img')?.forEach((img) => {
|
||||||
@@ -991,28 +985,28 @@ const ui = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let nameCell = document.createElement('div');
|
const nameCell = document.createElement('div');
|
||||||
let name = selectedCardFromList[index].getElementsByTagName('span').item(0)?.cloneNode(true);
|
const name = selectedCardFromList[index].getElementsByTagName('span').item(0)?.cloneNode(true);
|
||||||
if (name) {
|
if (name) {
|
||||||
nameCell.appendChild(name);
|
nameCell.appendChild(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'file-item';
|
div.className = 'file-item';
|
||||||
div.appendChild(iconCell);
|
div.appendChild(iconCell);
|
||||||
div.appendChild(nameCell);
|
div.appendChild(nameCell);
|
||||||
|
|
||||||
self.draggedItems.appendChild(div);
|
this.draggedItems.appendChild(div);
|
||||||
index += 1;
|
index += 1;
|
||||||
lastItemDiv = div;
|
lastItemDiv = div;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if more than 1 item, display the badge
|
// if more than 1 item, display the badge
|
||||||
if (selectedCardFromList.length > 1) {
|
if (selectedCardFromList.length > 1) {
|
||||||
let badge = document.createElement('span');
|
const badge = document.createElement('span');
|
||||||
badge.className = 'dragged-items-badge';
|
badge.className = 'dragged-items-badge';
|
||||||
badge.innerText = `${selectedCardFromList.length}`;
|
badge.innerText = `${selectedCardFromList.length}`;
|
||||||
self.draggedItems.appendChild(badge);
|
this.draggedItems.appendChild(badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if more than maxElements display the fading
|
// if more than maxElements display the fading
|
||||||
@@ -1020,14 +1014,16 @@ const ui = {
|
|||||||
lastItemDiv.classList.add('fading');
|
lastItemDiv.classList.add('fading');
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dragPreview.appendChild(self.draggedItems);
|
this.dragPreview.appendChild(this.draggedItems);
|
||||||
e.dataTransfer.setDragImage(self.draggedItems, 0, 0);
|
e.dataTransfer.setDragImage(this.draggedItems, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// dragend
|
// dragend
|
||||||
filesList.addEventListener('dragend', (e) => {
|
filesList.addEventListener('dragend', (_e) => {
|
||||||
self.dragPreview.removeChild(self.draggedItems);
|
this.dragPreview.removeChild(this.draggedItems);
|
||||||
document.querySelectorAll('.drop-target').forEach((el) => el.classList.remove('drop-target'));
|
document.querySelectorAll('.drop-target').forEach((el) => {
|
||||||
|
el.classList.remove('drop-target');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// dragover – only folders are valid drop targets
|
// dragover – only folders are valid drop targets
|
||||||
@@ -1057,7 +1053,7 @@ const ui = {
|
|||||||
card.classList.remove('drop-target');
|
card.classList.remove('drop-target');
|
||||||
|
|
||||||
const action = e.dataTransfer.dropEffect;
|
const action = e.dataTransfer.dropEffect;
|
||||||
await self._dropToFolder(action, targetFolderId, e.dataTransfer);
|
await this._dropToFolder(action, targetFolderId, e.dataTransfer);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1111,8 +1107,8 @@ const ui = {
|
|||||||
|
|
||||||
// SVG icon path (after icons.js replacement)
|
// SVG icon path (after icons.js replacement)
|
||||||
const svg = starBtn.querySelector('svg');
|
const svg = starBtn.querySelector('svg');
|
||||||
const filledPath = window.OxiIcons && window.OxiIcons['star'];
|
const filledPath = window.OxiIcons?.star;
|
||||||
const outlinePath = window.OxiIcons && window.OxiIcons['star-outline'];
|
const outlinePath = window.OxiIcons?.['star-outline'];
|
||||||
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');
|
||||||
@@ -1159,7 +1155,7 @@ const ui = {
|
|||||||
el.dataset.folderName = folder.name;
|
el.dataset.folderName = folder.name;
|
||||||
el.dataset.parentId = folder.parent_id || '';
|
el.dataset.parentId = folder.parent_id || '';
|
||||||
|
|
||||||
const isFav = window.favorites && window.favorites.isFavorite(folder.id, 'folder');
|
const isFav = window.favorites?.isFavorite(folder.id, 'folder');
|
||||||
const formattedDate = window.formatDateTime(folder.modified_at);
|
const formattedDate = window.formatDateTime(folder.modified_at);
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
@@ -1203,7 +1199,7 @@ const ui = {
|
|||||||
: 'Document';
|
: 'Document';
|
||||||
const fileSize = file.size_formatted || window.formatFileSize(file.size);
|
const fileSize = file.size_formatted || window.formatFileSize(file.size);
|
||||||
const formattedDate = window.formatDateTime(file.modified_at);
|
const formattedDate = window.formatDateTime(file.modified_at);
|
||||||
const isFav = window.favorites && window.favorites.isFavorite(file.id, 'file');
|
const isFav = window.favorites?.isFavorite(file.id, 'file');
|
||||||
|
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'file-item';
|
el.className = 'file-item';
|
||||||
@@ -1213,7 +1209,7 @@ const ui = {
|
|||||||
el.setAttribute('draggable', 'true');
|
el.setAttribute('draggable', 'true');
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
|
|
||||||
<div class="checkbox-cell"><input type="checkbox" class="item-checkbox"></div>
|
<div class="checkbox-cell"><input type="checkbox" class="item-checkbox"></div>
|
||||||
<div class="name-cell">
|
<div class="name-cell">
|
||||||
<div class="file-icon ${iconSpecialClass}">
|
<div class="file-icon ${iconSpecialClass}">
|
||||||
@@ -1262,7 +1258,7 @@ const ui = {
|
|||||||
<div></div><!-- actions -->
|
<div></div><!-- actions -->
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
if (window.i18n && window.i18n.translateElement) window.i18n.translateElement(filesList);
|
if (window.i18n?.translateElement) window.i18n.translateElement(filesList);
|
||||||
|
|
||||||
filesList.classList.remove('hidden');
|
filesList.classList.remove('hidden');
|
||||||
filesContainerError?.classList.add('hidden');
|
filesContainerError?.classList.add('hidden');
|
||||||
@@ -1286,7 +1282,7 @@ const ui = {
|
|||||||
const filesList = document.getElementById('files-list');
|
const filesList = document.getElementById('files-list');
|
||||||
if (filesContainerError) filesContainerError.innerHTML = content;
|
if (filesContainerError) filesContainerError.innerHTML = content;
|
||||||
|
|
||||||
if (window.i18n && window.i18n.translateElement) window.i18n.translateElement(filesContainerError);
|
if (window.i18n?.translateElement) window.i18n.translateElement(filesContainerError);
|
||||||
|
|
||||||
filesContainerError?.classList.remove('hidden');
|
filesContainerError?.classList.remove('hidden');
|
||||||
filesList?.classList.add('hidden');
|
filesList?.classList.add('hidden');
|
||||||
@@ -1384,7 +1380,9 @@ function toggleCardSelection(card, event) {
|
|||||||
*/
|
*/
|
||||||
function showContextMenuAtElement(triggerElement, menuId) {
|
function showContextMenuAtElement(triggerElement, menuId) {
|
||||||
// Hide any open menus first
|
// Hide any open menus first
|
||||||
document.querySelectorAll('.context-menu').forEach((m) => (m.style.display = 'none'));
|
document.querySelectorAll('.context-menu').forEach((m) => {
|
||||||
|
m.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
const menu = document.getElementById(menuId);
|
const menu = document.getElementById(menuId);
|
||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
@@ -1406,7 +1404,7 @@ function showContextMenuAtElement(triggerElement, menuId) {
|
|||||||
window.contextMenus.syncFavoriteOptionLabels();
|
window.contextMenus.syncFavoriteOptionLabels();
|
||||||
}
|
}
|
||||||
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
||||||
window.contextMenus.syncWopiOptionVisibility().catch(function () {});
|
window.contextMenus.syncWopiOptionVisibility().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.style.left = `${left}px`;
|
menu.style.left = `${left}px`;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
const uiFileTypes = {
|
const uiFileTypes = {
|
||||||
// TODO: 'd better to use a canViw() method in inlineViewer
|
// TODO: 'd better to use a canViw() method in inlineViewer
|
||||||
isViewableFile(file) {
|
isViewableFile(file) {
|
||||||
if (!file || !file.mime_type) return false;
|
if (!file?.mime_type) return false;
|
||||||
if (file.mime_type.startsWith('image/')) return true;
|
if (file.mime_type.startsWith('image/')) return true;
|
||||||
if (file.mime_type === 'application/pdf') return true;
|
if (file.mime_type === 'application/pdf') return true;
|
||||||
if (file.mime_type.startsWith('audio/')) return true;
|
if (file.mime_type.startsWith('audio/')) return true;
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ function updateUserMenuData() {
|
|||||||
const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes;
|
const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes;
|
||||||
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
||||||
|
|
||||||
if (storageFill) storageFill.style.width = percentage + '%';
|
if (storageFill) storageFill.style.width = `${percentage}%`;
|
||||||
if (storageText) {
|
if (storageText) {
|
||||||
const used = window.formatFileSize(usedBytes);
|
const used = window.formatFileSize(usedBytes);
|
||||||
const total = window.formatQuotaSize(quotaBytes);
|
const total = window.formatQuotaSize(quotaBytes);
|
||||||
@@ -206,7 +206,7 @@ function showUserProfileModal() {
|
|||||||
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
||||||
const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e';
|
const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e';
|
||||||
|
|
||||||
const t = (key, fallback) => (window.i18n && window.i18n.t ? window.i18n.t(key) || fallback : fallback);
|
const t = (key, fallback) => (window.i18n?.t ? window.i18n.t(key) || fallback : fallback);
|
||||||
|
|
||||||
const existing = document.getElementById('profile-modal-overlay');
|
const existing = document.getElementById('profile-modal-overlay');
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
@@ -220,7 +220,7 @@ function showUserProfileModal() {
|
|||||||
<div class="about-modal-avatar">${initials}</div>
|
<div class="about-modal-avatar">${initials}</div>
|
||||||
<h3 class="about-modal-username">${username}</h3>
|
<h3 class="about-modal-username">${username}</h3>
|
||||||
<p class="about-modal-email">${email}</p>
|
<p class="about-modal-email">${email}</p>
|
||||||
<span class="about-modal-role ${role === 'admin' ? 'about-modal-role-admin' : 'about-modal-role-user'}">${role === 'admin' ? '🛡️ Admin' : '👤 ' + t('user_menu.role_user', 'User')}</span>
|
<span class="about-modal-role ${role === 'admin' ? 'about-modal-role-admin' : 'about-modal-role-user'}">${role === 'admin' ? '🛡️ Admin' : `👤 ${t('user_menu.role_user', 'User')}`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="about-modal-storage">
|
<div class="about-modal-storage">
|
||||||
<div class="about-modal-storage-label">
|
<div class="about-modal-storage-label">
|
||||||
@@ -240,7 +240,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 = 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function getCsrfToken() {
|
|||||||
return match ? match.split('=')[1] : '';
|
return match ? match.split('=')[1] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// biome-ignore lint/correctness/noUnusedVariables: global function
|
||||||
function getCsrfHeaders() {
|
function getCsrfHeaders() {
|
||||||
const token = getCsrfToken();
|
const token = getCsrfToken();
|
||||||
return token ? { 'X-CSRF-Token': token } : {};
|
return token ? { 'X-CSRF-Token': token } : {};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
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, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFileSize(bytes) {
|
function formatFileSize(bytes) {
|
||||||
@@ -15,7 +15,7 @@ function formatFileSize(bytes) {
|
|||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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).
|
||||||
@@ -34,14 +34,14 @@ function formatDateTime(value) {
|
|||||||
} else {
|
} else {
|
||||||
dateValue = new Date(value);
|
dateValue = new Date(value);
|
||||||
}
|
}
|
||||||
if (isNaN(dateValue.getTime())) return String(value);
|
if (Number.isNaN(dateValue.getTime())) return String(value);
|
||||||
return dateValue.toLocaleDateString() + ' ' + dateValue.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
return `${dateValue.toLocaleDateString()} ${dateValue.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
if (isNaN(dateValue.getTime())) return String(value);
|
if (Number.isNaN(dateValue.getTime())) return String(value);
|
||||||
return dateValue.toLocaleDateString(undefined, {
|
return dateValue.toLocaleDateString(undefined, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Current locale code (default to browser locale if available, fallback to English)
|
// Current locale code (default to browser locale if available, fallback to English)
|
||||||
let currentLocale = (navigator.language && navigator.language.substring(0, 2)) || (navigator.userLanguage && navigator.userLanguage.substring(0, 2)) || 'en';
|
let currentLocale = navigator.language?.substring(0, 2) || navigator.userLanguage?.substring(0, 2) || 'en';
|
||||||
|
|
||||||
// Supported locales (languages that have locale files on the server)
|
// Supported locales (languages that have locale files on the server)
|
||||||
// When a locale file is not found, the system gracefully falls back to English
|
// When a locale file is not found, the system gracefully falls back to English
|
||||||
@@ -93,6 +93,8 @@ function getNestedValue(obj, path) {
|
|||||||
* @param {object} params - Parameters to replace in the translation (e.g., {name: 'John'})
|
* @param {object} params - Parameters to replace in the translation (e.g., {name: 'John'})
|
||||||
* @returns {string} - The translated string or the key itself if not found
|
* @returns {string} - The translated string or the key itself if not found
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: global function
|
||||||
function t(key, params = {}) {
|
function t(key, params = {}) {
|
||||||
// Get translation from cache
|
// Get translation from cache
|
||||||
const localeData = translations[currentLocale];
|
const localeData = translations[currentLocale];
|
||||||
@@ -131,8 +133,8 @@ function t(key, params = {}) {
|
|||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
// Try fallback to English
|
// Try fallback to English
|
||||||
if (currentLocale !== 'en' && translations['en']) {
|
if (currentLocale !== 'en' && translations.en) {
|
||||||
let fallbackValue = getNestedValue(translations['en'], key);
|
let fallbackValue = getNestedValue(translations.en, key);
|
||||||
|
|
||||||
if (!fallbackValue) {
|
if (!fallbackValue) {
|
||||||
const aliasMap = {
|
const aliasMap = {
|
||||||
@@ -143,7 +145,7 @@ function t(key, params = {}) {
|
|||||||
};
|
};
|
||||||
const aliasKey = aliasMap[key];
|
const aliasKey = aliasMap[key];
|
||||||
if (aliasKey) {
|
if (aliasKey) {
|
||||||
fallbackValue = getNestedValue(translations['en'], aliasKey);
|
fallbackValue = getNestedValue(translations.en, aliasKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,8 +306,8 @@ function safeT(key, params = {}) {
|
|||||||
let value = getNestedValue(localeData, key);
|
let value = getNestedValue(localeData, key);
|
||||||
|
|
||||||
// Fallback to English
|
// Fallback to English
|
||||||
if (!value && currentLocale !== 'en' && translations['en']) {
|
if (!value && currentLocale !== 'en' && translations.en) {
|
||||||
value = getNestedValue(translations['en'], key);
|
value = getNestedValue(translations.en, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value) return key;
|
if (!value) return key;
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
* https://fontawesome.com/license/free
|
* https://fontawesome.com/license/free
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// ── Icon registry ──────────────────────────────────────────────
|
// ── Icon registry ──────────────────────────────────────────────
|
||||||
// Each entry: [viewBox-width, svg-path-d]
|
// Each entry: [viewBox-width, svg-path-d]
|
||||||
// All icons use viewBox="0 0 {width} 512" and fill="currentColor".
|
// All icons use viewBox="0 0 {width} 512" and fill="currentColor".
|
||||||
@@ -379,7 +377,7 @@ function oxiIcon(name, extraClass) {
|
|||||||
const entry = _ICONS[name];
|
const entry = _ICONS[name];
|
||||||
if (!entry) return '';
|
if (!entry) return '';
|
||||||
const [w, d] = entry;
|
const [w, d] = entry;
|
||||||
const cls = extraClass ? 'oxi-icon ' + extraClass : 'oxi-icon';
|
const cls = extraClass ? `oxi-icon ${extraClass}` : 'oxi-icon';
|
||||||
return `<svg class="${cls}" viewBox="0 0 ${w} 512" aria-hidden="true"><path fill="currentColor" d="${d}"/></svg>`;
|
return `<svg class="${cls}" viewBox="0 0 ${w} 512" aria-hidden="true"><path fill="currentColor" d="${d}"/></svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,8 +421,8 @@ function replaceIconsInElement(container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use outline variant if available and element uses "far"
|
// Use outline variant if available and element uses "far"
|
||||||
if (isRegular && _ICONS[iconName + '-outline']) {
|
if (isRegular && _ICONS[`${iconName}-outline`]) {
|
||||||
iconName = iconName + '-outline';
|
iconName = `${iconName}-outline`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!iconName || !_ICONS[iconName]) continue;
|
if (!iconName || !_ICONS[iconName]) continue;
|
||||||
@@ -433,12 +431,12 @@ function replaceIconsInElement(container) {
|
|||||||
|
|
||||||
// Build SVG element
|
// Build SVG element
|
||||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
svg.setAttribute('viewBox', '0 0 ' + w + ' 512');
|
svg.setAttribute('viewBox', `0 0 ${w} 512`);
|
||||||
svg.setAttribute('aria-hidden', 'true');
|
svg.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
let svgClass = 'oxi-icon';
|
let svgClass = 'oxi-icon';
|
||||||
if (isSpin) svgClass += ' oxi-icon-spin';
|
if (isSpin) svgClass += ' oxi-icon-spin';
|
||||||
if (extraClasses.length) svgClass += ' ' + extraClasses.join(' ');
|
if (extraClasses.length) svgClass += ` ${extraClasses.join(' ')}`;
|
||||||
svg.setAttribute('class', svgClass);
|
svg.setAttribute('class', svgClass);
|
||||||
|
|
||||||
// Copy inline style if present
|
// Copy inline style if present
|
||||||
@@ -479,7 +477,7 @@ window.OxiIcons = _ICONS;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Observe future mutations (dynamic renders, modals, etc.)
|
// Observe future mutations (dynamic renders, modals, etc.)
|
||||||
new MutationObserver(function (mutations) {
|
new MutationObserver((mutations) => {
|
||||||
if (raf) return;
|
if (raf) return;
|
||||||
for (let i = 0; i < mutations.length; i++) {
|
for (let i = 0; i < mutations.length; i++) {
|
||||||
if (mutations[i].addedNodes.length) {
|
if (mutations[i].addedNodes.length) {
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const notifications = (() => {
|
const notifications = (() => {
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/* ── state ──────────────────────────────────────────────── */
|
/* ── state ──────────────────────────────────────────────── */
|
||||||
let _badgeCount = 0;
|
let _badgeCount = 0;
|
||||||
let _batchSeq = 0;
|
let _batchSeq = 0;
|
||||||
@@ -126,7 +124,7 @@ const notifications = (() => {
|
|||||||
|
|
||||||
// If panel is closed, bump badge
|
// If panel is closed, bump badge
|
||||||
const wrapper = $('notif-wrapper');
|
const wrapper = $('notif-wrapper');
|
||||||
if (!wrapper || !wrapper.classList.contains('open')) {
|
if (!wrapper?.classList.contains('open')) {
|
||||||
_incrementBadge();
|
_incrementBadge();
|
||||||
}
|
}
|
||||||
_showEmptyIfNeeded();
|
_showEmptyIfNeeded();
|
||||||
@@ -141,7 +139,7 @@ const notifications = (() => {
|
|||||||
* @param {string} [folderName] root folder name (for folder uploads)
|
* @param {string} [folderName] root folder name (for folder uploads)
|
||||||
*/
|
*/
|
||||||
function addUploadBatch(totalFiles, folderName) {
|
function addUploadBatch(totalFiles, folderName) {
|
||||||
const batchId = 'batch-' + ++_batchSeq;
|
const batchId = `batch-${++_batchSeq}`;
|
||||||
const body = $('notif-panel-body');
|
const body = $('notif-panel-body');
|
||||||
if (!body) return batchId;
|
if (!body) return batchId;
|
||||||
|
|
||||||
@@ -217,9 +215,9 @@ const notifications = (() => {
|
|||||||
if (!shouldUpdate) return;
|
if (!shouldUpdate) return;
|
||||||
|
|
||||||
// Show just the file name being uploaded (truncate long paths)
|
// Show just the file name being uploaded (truncate long paths)
|
||||||
const curEl = $(batchId + '-current');
|
const curEl = $(`${batchId}-current`);
|
||||||
if (curEl) {
|
if (curEl) {
|
||||||
const shortName = fileName.length > 50 ? '…' + fileName.slice(-49) : fileName;
|
const shortName = fileName.length > 50 ? `…${fileName.slice(-49)}` : fileName;
|
||||||
curEl.textContent = shortName;
|
curEl.textContent = shortName;
|
||||||
}
|
}
|
||||||
batch.lastLabelFile = fileName;
|
batch.lastLabelFile = fileName;
|
||||||
@@ -228,10 +226,10 @@ const notifications = (() => {
|
|||||||
// Update progress bar with per-file granularity:
|
// Update progress bar with per-file granularity:
|
||||||
// overall% = (completed_files + current_file_fraction) / total_files
|
// overall% = (completed_files + current_file_fraction) / total_files
|
||||||
const overallPct = Math.round(((batch.completed + pct / 100) / batch.totalFiles) * 100);
|
const overallPct = Math.round(((batch.completed + pct / 100) / batch.totalFiles) * 100);
|
||||||
const fillEl = $(batchId + '-fill');
|
const fillEl = $(`${batchId}-fill`);
|
||||||
const pctEl = $(batchId + '-pct');
|
const pctEl = $(`${batchId}-pct`);
|
||||||
if (fillEl) fillEl.style.width = overallPct + '%';
|
if (fillEl) fillEl.style.width = `${overallPct}%`;
|
||||||
if (pctEl) pctEl.textContent = overallPct + '%';
|
if (pctEl) pctEl.textContent = `${overallPct}%`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,15 +248,15 @@ const notifications = (() => {
|
|||||||
if (!isLast && batch.completed % 5 !== 0) return;
|
if (!isLast && batch.completed % 5 !== 0) return;
|
||||||
|
|
||||||
const pctVal = Math.round((batch.completed / batch.totalFiles) * 100);
|
const pctVal = Math.round((batch.completed / batch.totalFiles) * 100);
|
||||||
const fillEl = $(batchId + '-fill');
|
const fillEl = $(`${batchId}-fill`);
|
||||||
const pctEl = $(batchId + '-pct');
|
const pctEl = $(`${batchId}-pct`);
|
||||||
const statsEl = $(batchId + '-stats');
|
const statsEl = $(`${batchId}-stats`);
|
||||||
|
|
||||||
const t = window.i18n?.t || ((k) => k);
|
const t = window.i18n?.t || ((k) => k);
|
||||||
const filesLabel = t('upload.files');
|
const filesLabel = t('upload.files');
|
||||||
|
|
||||||
if (fillEl) fillEl.style.width = pctVal + '%';
|
if (fillEl) fillEl.style.width = `${pctVal}%`;
|
||||||
if (pctEl) pctEl.textContent = pctVal + '%';
|
if (pctEl) pctEl.textContent = `${pctVal}%`;
|
||||||
if (statsEl) statsEl.textContent = `${batch.completed} / ${batch.totalFiles} ${filesLabel}`;
|
if (statsEl) statsEl.textContent = `${batch.completed} / ${batch.totalFiles} ${filesLabel}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +267,7 @@ const notifications = (() => {
|
|||||||
const batch = _batches[batchId];
|
const batch = _batches[batchId];
|
||||||
if (!batch) return;
|
if (!batch) return;
|
||||||
|
|
||||||
const fillEl = $(batchId + '-fill');
|
const fillEl = $(`${batchId}-fill`);
|
||||||
if (fillEl) {
|
if (fillEl) {
|
||||||
fillEl.style.width = '100%';
|
fillEl.style.width = '100%';
|
||||||
fillEl.classList.add(successCount === totalFiles ? 'done' : 'error');
|
fillEl.classList.add(successCount === totalFiles ? 'done' : 'error');
|
||||||
@@ -279,11 +277,10 @@ const notifications = (() => {
|
|||||||
const iconEl = batch.el.querySelector('.notif-item-icon');
|
const iconEl = batch.el.querySelector('.notif-item-icon');
|
||||||
|
|
||||||
// Clear the current-file label
|
// Clear the current-file label
|
||||||
const curEl = $(batchId + '-current');
|
const curEl = $(`${batchId}-current`);
|
||||||
if (curEl) curEl.textContent = '';
|
if (curEl) curEl.textContent = '';
|
||||||
|
|
||||||
const t = window.i18n?.t || ((k) => k);
|
const t = window.i18n?.t || ((k) => k);
|
||||||
const filesLabel = t('upload.files');
|
|
||||||
const completeText = t('upload.complete', {
|
const completeText = t('upload.complete', {
|
||||||
count: successCount,
|
count: successCount,
|
||||||
total: totalFiles
|
total: totalFiles
|
||||||
@@ -302,7 +299,7 @@ const notifications = (() => {
|
|||||||
|
|
||||||
// If the panel is closed, bump badge
|
// If the panel is closed, bump badge
|
||||||
const wrapper = $('notif-wrapper');
|
const wrapper = $('notif-wrapper');
|
||||||
if (!wrapper || !wrapper.classList.contains('open')) {
|
if (!wrapper?.classList.contains('open')) {
|
||||||
_incrementBadge();
|
_incrementBadge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,7 +309,9 @@ const notifications = (() => {
|
|||||||
const body = $('notif-panel-body');
|
const body = $('notif-panel-body');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
// Remove all notif-items
|
// Remove all notif-items
|
||||||
body.querySelectorAll('.notif-item').forEach((el) => el.remove());
|
body.querySelectorAll('.notif-item').forEach((el) => {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
_clearBadge();
|
_clearBadge();
|
||||||
_showEmptyIfNeeded();
|
_showEmptyIfNeeded();
|
||||||
// automatically close notification center on clear
|
// automatically close notification center on clear
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// Service Worker registration — runs after page load.
|
// Service Worker registration — runs after page load.
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', () => {
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register('/sw.js')
|
.register('/sw.js')
|
||||||
.then(function () {
|
.then(() => {
|
||||||
/* registered */
|
/* registered */
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch((err) => {
|
||||||
console.log('Service Worker registration failed:', err);
|
console.log('Service Worker registration failed:', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ function detectBrowserLanguage() {
|
|||||||
// Build a language option element (card style)
|
// Build a language option element (card style)
|
||||||
function buildLanguageCard(lang, isSelected) {
|
function buildLanguageCard(lang, isSelected) {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'lang-picker-item' + (isSelected ? ' selected' : '');
|
item.className = `lang-picker-item${isSelected ? ' selected' : ''}`;
|
||||||
item.setAttribute('data-lang', lang.code);
|
item.setAttribute('data-lang', lang.code);
|
||||||
item.setAttribute('role', 'option');
|
item.setAttribute('role', 'option');
|
||||||
item.setAttribute('aria-selected', isSelected);
|
item.setAttribute('aria-selected', isSelected);
|
||||||
@@ -397,7 +397,6 @@ function initLanguageSelector() {
|
|||||||
const continueBtn = document.getElementById('language-continue');
|
const continueBtn = document.getElementById('language-continue');
|
||||||
const picker = document.getElementById('lang-picker');
|
const picker = document.getElementById('lang-picker');
|
||||||
const pickerSelected = document.getElementById('lang-picker-selected');
|
const pickerSelected = document.getElementById('lang-picker-selected');
|
||||||
const pickerDropdown = document.getElementById('lang-picker-dropdown');
|
|
||||||
const pickerList = document.getElementById('lang-picker-list');
|
const pickerList = document.getElementById('lang-picker-list');
|
||||||
const pickerFlag = document.getElementById('lang-picker-flag');
|
const pickerFlag = document.getElementById('lang-picker-flag');
|
||||||
const pickerName = document.getElementById('lang-picker-name');
|
const pickerName = document.getElementById('lang-picker-name');
|
||||||
@@ -510,7 +509,7 @@ function initLanguageSelector() {
|
|||||||
localStorage.setItem(FIRST_RUN_KEY, 'true');
|
localStorage.setItem(FIRST_RUN_KEY, 'true');
|
||||||
|
|
||||||
// Update i18n if available
|
// Update i18n if available
|
||||||
if (window.i18n && window.i18n.setLocale) {
|
if (window.i18n?.setLocale) {
|
||||||
await window.i18n.setLocale(selectedLanguage);
|
await window.i18n.setLocale(selectedLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,7 +632,7 @@ async function configureOidcLoginUI() {
|
|||||||
// Update button text with provider name
|
// Update button text with provider name
|
||||||
const btnTextEl = oidcBtn.querySelector('span');
|
const btnTextEl = oidcBtn.querySelector('span');
|
||||||
if (btnTextEl && oidcInfo.provider_name) {
|
if (btnTextEl && oidcInfo.provider_name) {
|
||||||
const template = window.i18n && window.i18n.t ? window.i18n.t('auth.sso_login_provider') : 'Sign in with {{provider}}';
|
const template = window.i18n?.t ? window.i18n.t('auth.sso_login_provider') : 'Sign in with {{provider}}';
|
||||||
btnTextEl.textContent = template.replace('{{provider}}', oidcInfo.provider_name);
|
btnTextEl.textContent = template.replace('{{provider}}', oidcInfo.provider_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +657,7 @@ async function configureOidcLoginUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DOM elements
|
// DOM elements
|
||||||
let loginPanel, registerPanel, adminSetupPanel, languagePanel;
|
let loginPanel, registerPanel, adminSetupPanel;
|
||||||
let loginForm, registerForm, adminSetupForm;
|
let loginForm, registerForm, adminSetupForm;
|
||||||
let loginError, registerError, registerSuccess, adminSetupError;
|
let loginError, registerError, registerSuccess, adminSetupError;
|
||||||
|
|
||||||
@@ -725,7 +724,7 @@ let authInitialized = false;
|
|||||||
// and clear auth data to break the loop
|
// and clear auth data to break the loop
|
||||||
(() => {
|
(() => {
|
||||||
// Check if we're being redirected in a loop
|
// Check if we're being redirected in a loop
|
||||||
const refreshAttempts = parseInt(localStorage.getItem('refresh_attempts') || '0');
|
const refreshAttempts = parseInt(localStorage.getItem('refresh_attempts') || '0', 10);
|
||||||
const redirectSource = new URLSearchParams(window.location.search).get('source');
|
const redirectSource = new URLSearchParams(window.location.search).get('source');
|
||||||
|
|
||||||
// Case 1: High refresh attempts
|
// Case 1: High refresh attempts
|
||||||
@@ -749,7 +748,7 @@ let authInitialized = false;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Case 3: Multiple redirects in short time
|
// Case 3: Multiple redirects in short time
|
||||||
const lastCleanup = parseInt(localStorage.getItem('last_emergency_clean') || '0');
|
const lastCleanup = parseInt(localStorage.getItem('last_emergency_clean') || '0', 10);
|
||||||
const timeSinceCleanup = Date.now() - lastCleanup;
|
const timeSinceCleanup = Date.now() - lastCleanup;
|
||||||
|
|
||||||
if (lastCleanup > 0 && timeSinceCleanup < 10000) {
|
if (lastCleanup > 0 && timeSinceCleanup < 10000) {
|
||||||
@@ -931,7 +930,7 @@ if (isLoginPage && registerForm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await register(username, email, password);
|
await register(username, email, password);
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
|
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
|
||||||
@@ -992,7 +991,7 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
const err = await response.json().catch(() => ({}));
|
const err = await response.json().catch(() => ({}));
|
||||||
throw new Error(err.message || 'Setup failed');
|
throw new Error(err.message || 'Setup failed');
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
await response.json();
|
||||||
|
|
||||||
// Show success message in the GUI instead of alert
|
// Show success message in the GUI instead of alert
|
||||||
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
|
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
|
||||||
@@ -1048,7 +1047,7 @@ async function login(username, password) {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.error || 'Authentication failed');
|
throw new Error(errorData.error || 'Authentication failed');
|
||||||
} catch (jsonError) {
|
} catch (_jsonError) {
|
||||||
// If the error response is not valid JSON
|
// If the error response is not valid JSON
|
||||||
throw new Error(`Authentication error (${response.status}): ${response.statusText}`);
|
throw new Error(`Authentication error (${response.status}): ${response.statusText}`);
|
||||||
}
|
}
|
||||||
@@ -1092,7 +1091,7 @@ async function register(username, email, password, role = 'user') {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.error || 'Registration error');
|
throw new Error(errorData.error || 'Registration error');
|
||||||
} catch (jsonError) {
|
} catch (_jsonError) {
|
||||||
// If the error response is not valid JSON
|
// If the error response is not valid JSON
|
||||||
throw new Error(`Registration error (${response.status}): ${response.statusText}`);
|
throw new Error(`Registration error (${response.status}): ${response.statusText}`);
|
||||||
}
|
}
|
||||||
@@ -1116,6 +1115,8 @@ async function register(username, email, password, role = 'user') {
|
|||||||
/**
|
/**
|
||||||
* Fetch current user data — relies on HttpOnly cookie (auto-sent).
|
* Fetch current user data — relies on HttpOnly cookie (auto-sent).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: global function
|
||||||
async function fetchUserData() {
|
async function fetchUserData() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(ME_ENDPOINT, {
|
const response = await fetch(ME_ENDPOINT, {
|
||||||
@@ -1142,7 +1143,7 @@ async function fetchUserData() {
|
|||||||
async function refreshAuthToken() {
|
async function refreshAuthToken() {
|
||||||
try {
|
try {
|
||||||
// Loop-breaker
|
// Loop-breaker
|
||||||
const refreshAttempts = parseInt(localStorage.getItem('refresh_attempts') || '0');
|
const refreshAttempts = parseInt(localStorage.getItem('refresh_attempts') || '0', 10);
|
||||||
localStorage.setItem('refresh_attempts', (refreshAttempts + 1).toString());
|
localStorage.setItem('refresh_attempts', (refreshAttempts + 1).toString());
|
||||||
|
|
||||||
if (refreshAttempts > 3) {
|
if (refreshAttempts > 3) {
|
||||||
@@ -1232,6 +1233,8 @@ function redirectToMainApp() {
|
|||||||
/**
|
/**
|
||||||
* Logout — tell the server to clear HttpOnly cookies, then redirect.
|
* Logout — tell the server to clear HttpOnly cookies, then redirect.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: global function
|
||||||
async function logout() {
|
async function logout() {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/auth/logout', {
|
await fetch('/api/auth/logout', {
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ const contextMenus = {
|
|||||||
const wopiEditTab = document.getElementById('wopi-edit-file-tab-option');
|
const wopiEditTab = document.getElementById('wopi-edit-file-tab-option');
|
||||||
if (!wopiEdit || !wopiEditTab) return;
|
if (!wopiEdit || !wopiEditTab) return;
|
||||||
|
|
||||||
const targetFile = window.app && window.app.contextMenuTargetFile;
|
const targetFile = window.app?.contextMenuTargetFile;
|
||||||
// Don't show WOPI editor for image files - they should use inline preview
|
// Don't show WOPI editor for image files - they should use inline preview
|
||||||
const isImage = targetFile && targetFile.mime_type && targetFile.mime_type.startsWith('image/');
|
const isImage = targetFile?.mime_type?.startsWith('image/');
|
||||||
const show = targetFile && !isImage && window.wopiEditor && (await window.wopiEditor.canEdit(targetFile.name));
|
const show = targetFile && !isImage && window.wopiEditor && (await window.wopiEditor.canEdit(targetFile.name));
|
||||||
|
|
||||||
wopiEdit.classList.toggle('hidden', !show);
|
wopiEdit.classList.toggle('hidden', !show);
|
||||||
@@ -37,8 +37,8 @@ const contextMenus = {
|
|||||||
syncFavoriteOptionLabels() {
|
syncFavoriteOptionLabels() {
|
||||||
if (!window.favorites) return;
|
if (!window.favorites) return;
|
||||||
|
|
||||||
const targetFile = window.app && window.app.contextMenuTargetFile;
|
const targetFile = window.app?.contextMenuTargetFile;
|
||||||
const targetFolder = window.app && window.app.contextMenuTargetFolder;
|
const targetFolder = window.app?.contextMenuTargetFolder;
|
||||||
|
|
||||||
if (targetFile) {
|
if (targetFile) {
|
||||||
const isFav = window.favorites.isFavorite(targetFile.id, 'file');
|
const isFav = window.favorites.isFavorite(targetFile.id, 'file');
|
||||||
@@ -68,7 +68,7 @@ const contextMenus = {
|
|||||||
const folder = window.app.contextMenuTargetFolder;
|
const folder = window.app.contextMenuTargetFolder;
|
||||||
|
|
||||||
// Check if folder is already in favorites to toggle
|
// Check if folder is already in favorites to toggle
|
||||||
if (window.favorites && window.favorites.isFavorite(folder.id, 'folder')) {
|
if (window.favorites?.isFavorite(folder.id, 'folder')) {
|
||||||
// Remove from favorites
|
// Remove from favorites
|
||||||
const ok = await window.favorites.removeFromFavorites(folder.id, 'folder');
|
const ok = await window.favorites.removeFromFavorites(folder.id, 'folder');
|
||||||
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
||||||
@@ -127,7 +127,7 @@ const contextMenus = {
|
|||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((fileDetails) => {
|
.then((fileDetails) => {
|
||||||
// Check if viewable file type (images, PDFs, text files)
|
// Check if viewable file type (images, PDFs, text files)
|
||||||
if (window.ui && window.ui.isViewableFile(fileDetails)) {
|
if (window.ui?.isViewableFile(fileDetails)) {
|
||||||
// Open with inline viewer
|
// Open with inline viewer
|
||||||
if (window.inlineViewer) {
|
if (window.inlineViewer) {
|
||||||
window.inlineViewer.openFile(fileDetails);
|
window.inlineViewer.openFile(fileDetails);
|
||||||
@@ -177,7 +177,7 @@ const contextMenus = {
|
|||||||
const file = window.app.contextMenuTargetFile;
|
const file = window.app.contextMenuTargetFile;
|
||||||
|
|
||||||
// Check if file is already in favorites to toggle
|
// Check if file is already in favorites to toggle
|
||||||
if (window.favorites && window.favorites.isFavorite(file.id, 'file')) {
|
if (window.favorites?.isFavorite(file.id, 'file')) {
|
||||||
// Remove from favorites
|
// Remove from favorites
|
||||||
const ok = await window.favorites.removeFromFavorites(file.id, 'file');
|
const ok = await window.favorites.removeFromFavorites(file.id, 'file');
|
||||||
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
if (ok && window.ui && typeof window.ui.setFavoriteVisualState === 'function') {
|
||||||
@@ -273,7 +273,7 @@ const contextMenus = {
|
|||||||
const fileIds = items.filter((i) => i.type === 'file').map((i) => i.id);
|
const fileIds = items.filter((i) => i.type === 'file').map((i) => i.id);
|
||||||
const folderIds = items.filter((i) => i.type === 'folder').map((i) => i.id);
|
const folderIds = items.filter((i) => i.type === 'folder').map((i) => i.id);
|
||||||
|
|
||||||
let result = await window.fileOps.batchCopy(fileIds, folderIds, targetId);
|
const result = await window.fileOps.batchCopy(fileIds, folderIds, targetId);
|
||||||
|
|
||||||
this.closeMoveDialog();
|
this.closeMoveDialog();
|
||||||
window.multiSelect.clear();
|
window.multiSelect.clear();
|
||||||
@@ -306,7 +306,7 @@ const contextMenus = {
|
|||||||
const fileIds = items.filter((i) => i.type === 'file').map((i) => i.id);
|
const fileIds = items.filter((i) => i.type === 'file').map((i) => i.id);
|
||||||
const folderIds = items.filter((i) => i.type === 'folder' && i.id !== targetId).map((i) => i.id);
|
const folderIds = items.filter((i) => i.type === 'folder' && i.id !== targetId).map((i) => i.id);
|
||||||
|
|
||||||
let result = await window.fileOps.batchMove(fileIds, folderIds, targetId);
|
const result = await window.fileOps.batchMove(fileIds, folderIds, targetId);
|
||||||
|
|
||||||
this.closeMoveDialog();
|
this.closeMoveDialog();
|
||||||
window.multiSelect.clear();
|
window.multiSelect.clear();
|
||||||
@@ -664,7 +664,7 @@ const contextMenus = {
|
|||||||
window.app.selectedTargetFolderId = parentFolderId || '';
|
window.app.selectedTargetFolderId = parentFolderId || '';
|
||||||
|
|
||||||
// Translate new elements
|
// Translate new elements
|
||||||
if (window.i18n && window.i18n.translateElement) {
|
if (window.i18n?.translateElement) {
|
||||||
window.i18n.translateElement(folderSelectContainer);
|
window.i18n.translateElement(folderSelectContainer);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -675,7 +675,7 @@ const contextMenus = {
|
|||||||
/**
|
/**
|
||||||
* Render breadcrumb navigation for move dialog
|
* Render breadcrumb navigation for move dialog
|
||||||
*/
|
*/
|
||||||
_renderMoveDialogBreadcrumb(container, breadcrumb, currentFolderId) {
|
_renderMoveDialogBreadcrumb(container, breadcrumb, _currentFolderId) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
@@ -757,6 +757,7 @@ const contextMenus = {
|
|||||||
// Use loadMoveDialogFolders which uses /api/folders/{id}/contents
|
// Use loadMoveDialogFolders which uses /api/folders/{id}/contents
|
||||||
await this.loadMoveDialogFolders(window.app.userHomeFolderId || null);
|
await this.loadMoveDialogFolders(window.app.userHomeFolderId || null);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show share dialog for files or folders
|
* Show share dialog for files or folders
|
||||||
* @param {Object} item - File or folder object
|
* @param {Object} item - File or folder object
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ const fileOps = {
|
|||||||
|
|
||||||
// Legacy dropzone bar
|
// Legacy dropzone bar
|
||||||
if (progressBar) {
|
if (progressBar) {
|
||||||
progressBar.style.width = (uploadedCount / totalFiles) * 100 + '%';
|
progressBar.style.width = `${(uploadedCount / totalFiles) * 100}%`;
|
||||||
}
|
}
|
||||||
// Notify bell of per-file completion
|
// Notify bell of per-file completion
|
||||||
if (window.notifications && batchId) {
|
if (window.notifications && batchId) {
|
||||||
@@ -601,7 +601,7 @@ const fileOps = {
|
|||||||
|
|
||||||
result = await this._uploadFileFetch(formData, thisTimeout);
|
result = await this._uploadFileFetch(formData, thisTimeout);
|
||||||
|
|
||||||
console.log(`[UPLOAD END] #${idx} ${rel} ok=${result.ok}${result.errorMsg ? ' err=' + result.errorMsg : ''}`);
|
console.log(`[UPLOAD END] #${idx} ${rel} ok=${result.ok}${result.errorMsg ? ` err=${result.errorMsg}` : ''}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = {
|
result = {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -618,7 +618,7 @@ const fileOps = {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (progressBar && uploadedCount % 10 === 0) {
|
if (progressBar && uploadedCount % 10 === 0) {
|
||||||
progressBar.style.width = (uploadedCount / totalFiles) * 100 + '%';
|
progressBar.style.width = `${(uploadedCount / totalFiles) * 100}%`;
|
||||||
}
|
}
|
||||||
if (uploadedCount % 50 === 0 || uploadedCount === totalFiles) {
|
if (uploadedCount % 50 === 0 || uploadedCount === totalFiles) {
|
||||||
console.log(`Progress: ${uploadedCount}/${totalFiles} (${successCount} ok)`);
|
console.log(`Progress: ${uploadedCount}/${totalFiles} (${successCount} ok)`);
|
||||||
@@ -748,7 +748,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
errorMessage = errorData.error || 'Unknown error';
|
errorMessage = errorData.error || 'Unknown error';
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
errorMessage = 'Error processing server response';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
|
||||||
@@ -790,7 +790,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
errorMessage = errorData.error || 'Unknown error';
|
errorMessage = errorData.error || 'Unknown error';
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
errorMessage = 'Error processing server response';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
|
||||||
@@ -808,7 +808,7 @@ const fileOps = {
|
|||||||
* @property {number} success number of files|folders sucessfully updated
|
* @property {number} success number of files|folders sucessfully updated
|
||||||
* @property {number} errors number of files|folders in error
|
* @property {number} errors number of files|folders in error
|
||||||
* /
|
* /
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move files & folders
|
* Move files & folders
|
||||||
* @param {string[]} fileIds - File IDs
|
* @param {string[]} fileIds - File IDs
|
||||||
@@ -883,7 +883,7 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
await response.json();
|
||||||
// Reload files after copying
|
// Reload files after copying
|
||||||
await window.loadFiles();
|
await window.loadFiles();
|
||||||
window.ui.showNotification('File copied', 'File copied successfully');
|
window.ui.showNotification('File copied', 'File copied successfully');
|
||||||
@@ -893,7 +893,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
errorMessage = errorData.error || 'Unknown error';
|
errorMessage = errorData.error || 'Unknown error';
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
errorMessage = 'Error processing server response';
|
errorMessage = 'Error processing server response';
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error copying the file: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error copying the file: ${errorMessage}`);
|
||||||
@@ -997,7 +997,7 @@ const fileOps = {
|
|||||||
try {
|
try {
|
||||||
const errorData = JSON.parse(errorText);
|
const errorData = JSON.parse(errorText);
|
||||||
errorMessage = errorData.error || response.statusText;
|
errorMessage = errorData.error || response.statusText;
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
errorMessage = errorText || response.statusText;
|
errorMessage = errorText || response.statusText;
|
||||||
}
|
}
|
||||||
window.ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
|
window.ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
|
||||||
@@ -1043,7 +1043,7 @@ const fileOps = {
|
|||||||
// Try to parse as JSON
|
// Try to parse as JSON
|
||||||
const errorData = JSON.parse(errorText);
|
const errorData = JSON.parse(errorText);
|
||||||
errorMessage = errorData.error || response.statusText;
|
errorMessage = errorData.error || response.statusText;
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
// If not JSON, use text as is
|
// If not JSON, use text as is
|
||||||
errorMessage = errorText || response.statusText;
|
errorMessage = errorText || response.statusText;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class InlineViewer {
|
|||||||
// Detect images by mime type OR extension (uploads via WebDAV may lack correct mime)
|
// Detect images by mime type OR extension (uploads via WebDAV may lack correct mime)
|
||||||
const ext = (file.name || '').split('.').pop().toLowerCase();
|
const ext = (file.name || '').split('.').pop().toLowerCase();
|
||||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'heic', 'heif', 'avif', 'tiff'];
|
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'heic', 'heif', 'avif', 'tiff'];
|
||||||
const isImage = (file.mime_type && file.mime_type.startsWith('image/')) || imageExts.includes(ext);
|
const isImage = file.mime_type?.startsWith('image/') || imageExts.includes(ext);
|
||||||
if (!isImage && window.wopiEditor && (await window.wopiEditor.canEdit(file.name))) {
|
if (!isImage && window.wopiEditor && (await window.wopiEditor.canEdit(file.name))) {
|
||||||
try {
|
try {
|
||||||
window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
||||||
@@ -159,7 +159,7 @@ class InlineViewer {
|
|||||||
|
|
||||||
// Create text viewer using authenticated fetch
|
// Create text viewer using authenticated fetch
|
||||||
this.createTextViewer(file, container, loader);
|
this.createTextViewer(file, container, loader);
|
||||||
} else if (file.mime_type && file.mime_type.startsWith('audio/')) {
|
} else if (file.mime_type?.startsWith('audio/')) {
|
||||||
// Hide zoom controls for audio
|
// Hide zoom controls for audio
|
||||||
controls.style.display = 'none';
|
controls.style.display = 'none';
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ class InlineViewer {
|
|||||||
|
|
||||||
// Create audio player
|
// Create audio player
|
||||||
this.createMediaViewer(file, 'audio', container, loader);
|
this.createMediaViewer(file, 'audio', container, loader);
|
||||||
} else if (file.mime_type && file.mime_type.startsWith('video/')) {
|
} else if (file.mime_type?.startsWith('video/')) {
|
||||||
// Hide zoom controls for video
|
// Hide zoom controls for video
|
||||||
controls.style.display = 'none';
|
controls.style.display = 'none';
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ class InlineViewer {
|
|||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|
||||||
// Remove loader
|
// Remove loader
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@ class InlineViewer {
|
|||||||
console.error('Error creating text viewer:', error);
|
console.error('Error creating text viewer:', error);
|
||||||
|
|
||||||
// Remove loader
|
// Remove loader
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,8 +284,8 @@ class InlineViewer {
|
|||||||
// e.loaded and e.total are JavaScript numbers (64-bit float)
|
// e.loaded and e.total are JavaScript numbers (64-bit float)
|
||||||
const progress = e.loaded / e.total;
|
const progress = e.loaded / e.total;
|
||||||
const pct = Math.round(progress * 100);
|
const pct = Math.round(progress * 100);
|
||||||
progressBar.style.width = pct + '%';
|
progressBar.style.width = `${pct}%`;
|
||||||
progressText.textContent = pct + '%';
|
progressText.textContent = `${pct}%`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -299,7 +299,7 @@ class InlineViewer {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.onerror = function () {
|
xhr.onerror = () => {
|
||||||
reject(new Error('Network error'));
|
reject(new Error('Network error'));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,10 +310,10 @@ class InlineViewer {
|
|||||||
const blob = response;
|
const blob = response;
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
console.log('Created blob URL:', blobUrl.substring(0, 30) + '...');
|
console.log('Created blob URL:', `${blobUrl.substring(0, 30)}...`);
|
||||||
|
|
||||||
// Remove loader
|
// Remove loader
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ class InlineViewer {
|
|||||||
console.error('Error creating blob URL viewer:', error);
|
console.error('Error creating blob URL viewer:', error);
|
||||||
|
|
||||||
// Remove loader
|
// Remove loader
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ class InlineViewer {
|
|||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
// Remove loader
|
// Remove loader
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +472,7 @@ class InlineViewer {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error creating ${mediaType} viewer:`, error);
|
console.error(`Error creating ${mediaType} viewer:`, error);
|
||||||
|
|
||||||
if (loader && loader.parentNode) {
|
if (loader?.parentNode) {
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,85 +492,85 @@ class InlineViewer {
|
|||||||
<p>Try downloading it directly.</p>
|
<p>Try downloading it directly.</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(message);
|
container.appendChild(message);
|
||||||
}
|
|
||||||
|
|
||||||
closeViewer() {
|
|
||||||
// Get modal
|
|
||||||
const modal = document.getElementById('inline-viewer-modal');
|
|
||||||
|
|
||||||
// stops audio/video before closing viewver
|
|
||||||
const media = modal.querySelector('audio, video');
|
|
||||||
if (media && !media.paused) media.pause();
|
|
||||||
|
|
||||||
// Hide modal
|
|
||||||
modal.classList.remove('active');
|
|
||||||
|
|
||||||
// Clean up blob URL if exists
|
|
||||||
if (this.currentBlobUrl) {
|
|
||||||
URL.revokeObjectURL(this.currentBlobUrl);
|
|
||||||
this.currentBlobUrl = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear
|
closeViewer() {
|
||||||
window.app.viewFile = null;
|
// Get modal
|
||||||
window.updateHistory(false);
|
const modal = document.getElementById('inline-viewer-modal');
|
||||||
|
|
||||||
// Clear references
|
// stops audio/video before closing viewver
|
||||||
this.currentFile = null;
|
const media = modal.querySelector('audio, video');
|
||||||
}
|
if (media && !media.paused) media.pause();
|
||||||
|
|
||||||
downloadFile(file) {
|
// Hide modal
|
||||||
fetch(`/api/files/${file.id}`, { credentials: 'same-origin' })
|
modal.classList.remove('active');
|
||||||
.then(res => {
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
// Clean up blob URL if exists
|
||||||
return res.blob();
|
if (this.currentBlobUrl) {
|
||||||
})
|
URL.revokeObjectURL(this.currentBlobUrl);
|
||||||
.then(blob => {
|
this.currentBlobUrl = null;
|
||||||
const url = URL.createObjectURL(blob);
|
}
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
// clear
|
||||||
link.download = file.name;
|
window.app.viewFile = null;
|
||||||
document.body.appendChild(link);
|
window.updateHistory(false);
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
// Clear references
|
||||||
URL.revokeObjectURL(url);
|
this.currentFile = null;
|
||||||
})
|
}
|
||||||
.catch(err => console.error('Download error:', err));
|
|
||||||
}
|
downloadFile(file) {
|
||||||
|
fetch(`/api/files/${file.id}`, { credentials: 'same-origin' })
|
||||||
zoomImage(factor) {
|
.then((res) => {
|
||||||
const container = document.querySelector('.inline-viewer-container');
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const img = container.querySelector('.inline-viewer-image');
|
return res.blob();
|
||||||
|
})
|
||||||
if (!img) return;
|
.then((blob) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
// Get current scale
|
const link = document.createElement('a');
|
||||||
let scale = img.dataset.scale ? parseFloat(img.dataset.scale) : 1.0;
|
link.href = url;
|
||||||
|
link.download = file.name;
|
||||||
// Apply zoom factor
|
document.body.appendChild(link);
|
||||||
scale *= factor;
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
// Limit scale
|
URL.revokeObjectURL(url);
|
||||||
scale = Math.max(0.1, Math.min(5.0, scale));
|
})
|
||||||
|
.catch((err) => console.error('Download error:', err));
|
||||||
// Save scale
|
}
|
||||||
img.dataset.scale = scale;
|
|
||||||
|
zoomImage(factor) {
|
||||||
// Apply scale
|
const container = document.querySelector('.inline-viewer-container');
|
||||||
img.style.transform = `scale(${scale})`;
|
const img = container.querySelector('.inline-viewer-image');
|
||||||
}
|
|
||||||
|
if (!img) return;
|
||||||
resetZoom() {
|
|
||||||
const container = document.querySelector('.inline-viewer-container');
|
// Get current scale
|
||||||
const img = container.querySelector('.inline-viewer-image');
|
let scale = img.dataset.scale ? parseFloat(img.dataset.scale) : 1.0;
|
||||||
|
|
||||||
if (!img) return;
|
// Apply zoom factor
|
||||||
|
scale *= factor;
|
||||||
// Reset scale
|
|
||||||
img.dataset.scale = 1.0;
|
// Limit scale
|
||||||
img.style.transform = 'scale(1.0)';
|
scale = Math.max(0.1, Math.min(5.0, scale));
|
||||||
}
|
|
||||||
|
// Save scale
|
||||||
|
img.dataset.scale = scale;
|
||||||
|
|
||||||
|
// Apply scale
|
||||||
|
img.style.transform = `scale(${scale})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetZoom() {
|
||||||
|
const container = document.querySelector('.inline-viewer-container');
|
||||||
|
const img = container.querySelector('.inline-viewer-image');
|
||||||
|
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
// Reset scale
|
||||||
|
img.dataset.scale = 1.0;
|
||||||
|
img.style.transform = 'scale(1.0)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize viewer when document is ready
|
// Initialize viewer when document is ready
|
||||||
|
|||||||
@@ -72,8 +72,12 @@ const multiSelect = {
|
|||||||
clear() {
|
clear() {
|
||||||
this._selected.clear();
|
this._selected.clear();
|
||||||
this._lastClickedIndex = -1;
|
this._lastClickedIndex = -1;
|
||||||
document.querySelectorAll('.file-item.selected').forEach((el) => el.classList.remove('selected'));
|
document.querySelectorAll('.file-item.selected').forEach((el) => {
|
||||||
document.querySelectorAll('.item-checkbox').forEach((cb) => (cb.checked = false));
|
el.classList.remove('selected');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.item-checkbox').forEach((cb) => {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
this._syncUI();
|
this._syncUI();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -103,8 +107,8 @@ const multiSelect = {
|
|||||||
* @return {ItemSelection}
|
* @return {ItemSelection}
|
||||||
*/
|
*/
|
||||||
getSelection(targtFolderId) {
|
getSelection(targtFolderId) {
|
||||||
let fileIds = [];
|
const fileIds = [];
|
||||||
let folderIds = [];
|
const folderIds = [];
|
||||||
|
|
||||||
// TODO optimize & check if _selected is a better use
|
// TODO optimize & check if _selected is a better use
|
||||||
document.querySelectorAll(`div.file-item.selected`).forEach((item) => {
|
document.querySelectorAll(`div.file-item.selected`).forEach((item) => {
|
||||||
@@ -155,7 +159,9 @@ const multiSelect = {
|
|||||||
_selectAllInContainer(containerId, selector) {
|
_selectAllInContainer(containerId, selector) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.querySelectorAll(selector).forEach((el) => this._selectElement(el));
|
container.querySelectorAll(selector).forEach((el) => {
|
||||||
|
this._selectElement(el);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_getAllVisibleItems() {
|
_getAllVisibleItems() {
|
||||||
@@ -190,7 +196,7 @@ const multiSelect = {
|
|||||||
const info = this._extractInfo(el);
|
const info = this._extractInfo(el);
|
||||||
if (!info) return;
|
if (!info) return;
|
||||||
|
|
||||||
if (event && event.shiftKey && this._lastClickedIndex >= 0 && index >= 0) {
|
if (event?.shiftKey && this._lastClickedIndex >= 0 && index >= 0) {
|
||||||
const start = Math.min(this._lastClickedIndex, index);
|
const start = Math.min(this._lastClickedIndex, index);
|
||||||
const end = Math.max(this._lastClickedIndex, index);
|
const end = Math.max(this._lastClickedIndex, index);
|
||||||
for (let i = start; i <= end; i++) {
|
for (let i = start; i <= end; i++) {
|
||||||
@@ -200,7 +206,7 @@ const multiSelect = {
|
|||||||
const sel = iInfo.type === 'folder' ? `[data-folder-id="${iInfo.id}"]` : `[data-file-id="${iInfo.id}"]`;
|
const sel = iInfo.type === 'folder' ? `[data-folder-id="${iInfo.id}"]` : `[data-file-id="${iInfo.id}"]`;
|
||||||
document.querySelectorAll(sel).forEach((e) => {
|
document.querySelectorAll(sel).forEach((e) => {
|
||||||
e.classList.add('selected');
|
e.classList.add('selected');
|
||||||
let checkbox = e.querySelector('input[type="checkbox"]');
|
const checkbox = e.querySelector('input[type="checkbox"]');
|
||||||
if (checkbox) checkbox.checked = true;
|
if (checkbox) checkbox.checked = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -208,7 +214,7 @@ const multiSelect = {
|
|||||||
} else {
|
} else {
|
||||||
const nowSelected = this.toggle(info.id, info.name, info.type, info.parentId);
|
const nowSelected = this.toggle(info.id, info.name, info.type, info.parentId);
|
||||||
el.classList.toggle('selected', nowSelected);
|
el.classList.toggle('selected', nowSelected);
|
||||||
let checkbox = el.querySelector('input[type="checkbox"]');
|
const checkbox = el.querySelector('input[type="checkbox"]');
|
||||||
if (checkbox) checkbox.checked = nowSelected;
|
if (checkbox) checkbox.checked = nowSelected;
|
||||||
}
|
}
|
||||||
this._lastClickedIndex = index;
|
this._lastClickedIndex = index;
|
||||||
@@ -504,7 +510,7 @@ const multiSelect = {
|
|||||||
const batchSelectionBar = document.getElementById('batch-selection-bar');
|
const batchSelectionBar = document.getElementById('batch-selection-bar');
|
||||||
batchSelectionBar.innerHTML = this._buildSelectionBarHTML();
|
batchSelectionBar.innerHTML = this._buildSelectionBarHTML();
|
||||||
|
|
||||||
if (window.i18n && window.i18n.translateElement) {
|
if (window.i18n?.translateElement) {
|
||||||
window.i18n.translateElement(batchSelectionBar);
|
window.i18n.translateElement(batchSelectionBar);
|
||||||
}
|
}
|
||||||
this._wireBarButtons();
|
this._wireBarButtons();
|
||||||
@@ -512,10 +518,9 @@ const multiSelect = {
|
|||||||
|
|
||||||
// FIXME: competition with _
|
// FIXME: competition with _
|
||||||
_injectListHeaderCheckbox() {
|
_injectListHeaderCheckbox() {
|
||||||
const self = this;
|
|
||||||
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
||||||
if (!selectAllCheckbox) return;
|
if (!selectAllCheckbox) return;
|
||||||
selectAllCheckbox.addEventListener('change', () => self.toggleAll());
|
selectAllCheckbox.addEventListener('change', () => this.toggleAll());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const search = {
|
|||||||
try {
|
try {
|
||||||
const errorJson = await response.json();
|
const errorJson = await response.json();
|
||||||
errorText = errorJson.error || response.statusText;
|
errorText = errorJson.error || response.statusText;
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
errorText = response.statusText;
|
errorText = response.statusText;
|
||||||
}
|
}
|
||||||
console.error(`Search error: ${errorText}`);
|
console.error(`Search error: ${errorText}`);
|
||||||
@@ -142,7 +142,7 @@ const search = {
|
|||||||
if (sortSelect) {
|
if (sortSelect) {
|
||||||
sortSelect.addEventListener('change', () => {
|
sortSelect.addEventListener('change', () => {
|
||||||
const searchInput = document.querySelector('.search-container input');
|
const searchInput = document.querySelector('.search-container input');
|
||||||
if (searchInput && 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 }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ class WopiEditor {
|
|||||||
* Fetches supported extensions from the server (cached after first call).
|
* Fetches supported extensions from the server (cached after first call).
|
||||||
*/
|
*/
|
||||||
async canEdit(filename) {
|
async canEdit(filename) {
|
||||||
var ext = filename.split('.').pop().toLowerCase();
|
const ext = filename.split('.').pop().toLowerCase();
|
||||||
var supported = await this._getSupportedExtensions();
|
const supported = await this._getSupportedExtensions();
|
||||||
return supported.includes(ext);
|
return supported.includes(ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,8 +38,8 @@ class WopiEditor {
|
|||||||
async openInTab(fileId, fileName, action) {
|
async openInTab(fileId, fileName, action) {
|
||||||
action = action || 'edit';
|
action = action || 'edit';
|
||||||
try {
|
try {
|
||||||
var data = await this._getEditorUrlWithFallback(fileId, fileName, action);
|
const data = await this._getEditorUrlWithFallback(fileId, fileName, action);
|
||||||
var hostUrl = '/wopi/edit/' + encodeURIComponent(fileId) + '?access_token=' + encodeURIComponent(data.access_token);
|
const hostUrl = `/wopi/edit/${encodeURIComponent(fileId)}?access_token=${encodeURIComponent(data.access_token)}`;
|
||||||
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);
|
||||||
@@ -53,12 +53,12 @@ class WopiEditor {
|
|||||||
* Fetch editor URL and WOPI token from the backend.
|
* Fetch editor URL and WOPI token from the backend.
|
||||||
*/
|
*/
|
||||||
async _getEditorUrl(fileId, action) {
|
async _getEditorUrl(fileId, action) {
|
||||||
var response = await fetch('/api/wopi/editor-url?file_id=' + encodeURIComponent(fileId) + '&action=' + encodeURIComponent(action), {
|
const response = await fetch(`/api/wopi/editor-url?file_id=${encodeURIComponent(fileId)}&action=${encodeURIComponent(action)}`, {
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
var text = await response.text();
|
const text = await response.text();
|
||||||
throw new Error('Editor URL request failed: ' + response.status + ' ' + text);
|
throw new Error(`Editor URL request failed: ${response.status} ${text}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -94,55 +94,55 @@ class WopiEditor {
|
|||||||
_showModal(editorData, fileName) {
|
_showModal(editorData, fileName) {
|
||||||
this.closeEditor();
|
this.closeEditor();
|
||||||
|
|
||||||
var modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
modal.id = 'wopi-editor-modal';
|
modal.id = 'wopi-editor-modal';
|
||||||
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;background:#fff;';
|
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;background:#fff;';
|
||||||
|
|
||||||
var header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
header.style.cssText =
|
header.style.cssText =
|
||||||
'height:40px;background:#333;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-family:sans-serif;font-size:14px;';
|
'height:40px;background:#333;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-family:sans-serif;font-size:14px;';
|
||||||
|
|
||||||
var title = document.createElement('span');
|
const title = document.createElement('span');
|
||||||
title.textContent = fileName;
|
title.textContent = fileName;
|
||||||
header.appendChild(title);
|
header.appendChild(title);
|
||||||
|
|
||||||
var closeBtn = document.createElement('button');
|
const closeBtn = document.createElement('button');
|
||||||
closeBtn.textContent = '\u2715';
|
closeBtn.textContent = '\u2715';
|
||||||
closeBtn.style.cssText = 'background:none;border:none;color:#fff;cursor:pointer;font-size:18px;padding:4px 8px;';
|
closeBtn.style.cssText = 'background:none;border:none;color:#fff;cursor:pointer;font-size:18px;padding:4px 8px;';
|
||||||
closeBtn.onclick = this.closeEditor.bind(this);
|
closeBtn.onclick = this.closeEditor.bind(this);
|
||||||
header.appendChild(closeBtn);
|
header.appendChild(closeBtn);
|
||||||
|
|
||||||
var form = document.createElement('form');
|
const form = document.createElement('form');
|
||||||
form.id = 'wopi_form';
|
form.id = 'wopi_form';
|
||||||
form.target = 'wopi_frame';
|
form.target = 'wopi_frame';
|
||||||
form.action = editorData.editor_url;
|
form.action = editorData.editor_url;
|
||||||
form.method = 'post';
|
form.method = 'post';
|
||||||
form.style.display = 'none';
|
form.style.display = 'none';
|
||||||
|
|
||||||
var tokenInput = document.createElement('input');
|
const tokenInput = document.createElement('input');
|
||||||
tokenInput.name = 'access_token';
|
tokenInput.name = 'access_token';
|
||||||
tokenInput.value = editorData.access_token;
|
tokenInput.value = editorData.access_token;
|
||||||
tokenInput.type = 'hidden';
|
tokenInput.type = 'hidden';
|
||||||
form.appendChild(tokenInput);
|
form.appendChild(tokenInput);
|
||||||
|
|
||||||
var ttlInput = document.createElement('input');
|
const ttlInput = document.createElement('input');
|
||||||
ttlInput.name = 'access_token_ttl';
|
ttlInput.name = 'access_token_ttl';
|
||||||
ttlInput.value = editorData.access_token_ttl;
|
ttlInput.value = editorData.access_token_ttl;
|
||||||
ttlInput.type = 'hidden';
|
ttlInput.type = 'hidden';
|
||||||
form.appendChild(ttlInput);
|
form.appendChild(ttlInput);
|
||||||
|
|
||||||
var frameHolder = document.createElement('div');
|
const frameHolder = document.createElement('div');
|
||||||
frameHolder.style.cssText = 'position:absolute;top:40px;left:0;right:0;bottom:0;';
|
frameHolder.style.cssText = 'position:absolute;top:40px;left:0;right:0;bottom:0;';
|
||||||
|
|
||||||
// Loading spinner (removed once the editor signals ready)
|
// Loading spinner (removed once the editor signals ready)
|
||||||
var spinner = document.createElement('div');
|
const spinner = document.createElement('div');
|
||||||
spinner.id = 'wopi-loading-spinner';
|
spinner.id = 'wopi-loading-spinner';
|
||||||
spinner.style.cssText =
|
spinner.style.cssText =
|
||||||
'position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:#f5f5f5;z-index:1;';
|
'position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:#f5f5f5;z-index:1;';
|
||||||
spinner.innerHTML = '<i class="fas fa-spinner fa-spin empty-state-icon spinner"></i>';
|
spinner.innerHTML = '<i class="fas fa-spinner fa-spin empty-state-icon spinner"></i>';
|
||||||
frameHolder.appendChild(spinner);
|
frameHolder.appendChild(spinner);
|
||||||
|
|
||||||
var iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.name = 'wopi_frame';
|
iframe.name = 'wopi_frame';
|
||||||
iframe.title = 'Document Editor';
|
iframe.title = 'Document Editor';
|
||||||
iframe.style.cssText = 'width:100%;height:100%;border:none;';
|
iframe.style.cssText = 'width:100%;height:100%;border:none;';
|
||||||
@@ -175,9 +175,9 @@ class WopiEditor {
|
|||||||
if (msgId === 'UI_Close' || msgId === 'close') {
|
if (msgId === 'UI_Close' || msgId === 'close') {
|
||||||
this.closeEditor();
|
this.closeEditor();
|
||||||
} else if (msgId === 'App_LoadingStatus') {
|
} else if (msgId === 'App_LoadingStatus') {
|
||||||
var status = data.Values && data.Values.Status;
|
const status = data.Values?.Status;
|
||||||
if (status === 'Document_Loaded' || status === 'Frame_Ready') {
|
if (status === 'Document_Loaded' || status === 'Frame_Ready') {
|
||||||
var sp = document.getElementById('wopi-loading-spinner');
|
const sp = document.getElementById('wopi-loading-spinner');
|
||||||
if (sp) sp.remove();
|
if (sp) sp.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,9 +225,9 @@ class WopiEditor {
|
|||||||
*/
|
*/
|
||||||
async _fetchSupportedExtensions() {
|
async _fetchSupportedExtensions() {
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/wopi/supported-extensions');
|
const response = await fetch('/wopi/supported-extensions');
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
var exts = await response.json();
|
const exts = await response.json();
|
||||||
if (Array.isArray(exts) && exts.length > 0) {
|
if (Array.isArray(exts) && exts.length > 0) {
|
||||||
this._supportedExtensions = exts;
|
this._supportedExtensions = exts;
|
||||||
return exts;
|
return exts;
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const favorites = {
|
|||||||
await this._fetchFromServer();
|
await this._fetchFromServer();
|
||||||
|
|
||||||
// Notify user
|
// Notify user
|
||||||
if (window.ui && window.ui.showNotification) {
|
if (window.ui?.showNotification) {
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(
|
||||||
window.i18n ? window.i18n.t('favorites.added_title') : 'Added to favorites',
|
window.i18n ? window.i18n.t('favorites.added_title') : 'Added to favorites',
|
||||||
`"${name}" ${window.i18n ? window.i18n.t('favorites.added_msg') : 'added to favorites'}`
|
`"${name}" ${window.i18n ? window.i18n.t('favorites.added_msg') : 'added to favorites'}`
|
||||||
@@ -139,7 +139,7 @@ const favorites = {
|
|||||||
// Remove from local cache
|
// Remove from local cache
|
||||||
this._cache.delete(this._cacheKey(id, type));
|
this._cache.delete(this._cacheKey(id, type));
|
||||||
|
|
||||||
if (window.ui && window.ui.showNotification) {
|
if (window.ui?.showNotification) {
|
||||||
window.ui.showNotification(
|
window.ui.showNotification(
|
||||||
window.i18n ? window.i18n.t('favorites.removed_title') : 'Removed from favorites',
|
window.i18n ? window.i18n.t('favorites.removed_title') : 'Removed from favorites',
|
||||||
`"${itemName}" ${window.i18n ? window.i18n.t('favorites.removed_msg') : 'removed from favorites'}`
|
`"${itemName}" ${window.i18n ? window.i18n.t('favorites.removed_msg') : 'removed from favorites'}`
|
||||||
@@ -208,7 +208,7 @@ const favorites = {
|
|||||||
if (files.length) window.ui.renderFiles(files);
|
if (files.length) window.ui.renderFiles(files);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error displaying favorites:', error);
|
console.error('Error displaying favorites:', error);
|
||||||
if (window.ui && window.ui.showNotification) {
|
if (window.ui?.showNotification) {
|
||||||
window.ui.showNotification('Error', 'Error loading favorite items');
|
window.ui.showNotification('Error', 'Error loading favorite items');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ const photosView = {
|
|||||||
if (existingHeader) {
|
if (existingHeader) {
|
||||||
// Append tiles to existing grid and update count badge
|
// Append tiles to existing grid and update count badge
|
||||||
const grid = existingHeader.nextElementSibling;
|
const grid = existingHeader.nextElementSibling;
|
||||||
if (grid && 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 = grid.children.length;
|
||||||
@@ -228,7 +228,7 @@ const photosView = {
|
|||||||
|
|
||||||
/** Generate HTML for a single photo/video tile */
|
/** Generate HTML for a single photo/video tile */
|
||||||
_renderTile(file) {
|
_renderTile(file) {
|
||||||
const isVideo = file.mime_type && file.mime_type.startsWith('video/');
|
const isVideo = file.mime_type?.startsWith('video/');
|
||||||
const selected = this.selected.has(file.id) ? ' selected' : '';
|
const selected = this.selected.has(file.id) ? ' selected' : '';
|
||||||
const cachedThumb = isVideo && this._videoThumbCache.has(file.id) ? this._videoThumbCache.get(file.id) : null;
|
const cachedThumb = isVideo && this._videoThumbCache.has(file.id) ? this._videoThumbCache.get(file.id) : null;
|
||||||
const thumbUrl = cachedThumb || `/api/files/${file.id}/thumbnail/preview`;
|
const thumbUrl = cachedThumb || `/api/files/${file.id}/thumbnail/preview`;
|
||||||
@@ -359,7 +359,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 = { '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`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -527,7 +527,9 @@ const photosView = {
|
|||||||
|
|
||||||
bar.querySelector('#photos-sel-clear').onclick = () => {
|
bar.querySelector('#photos-sel-clear').onclick = () => {
|
||||||
this.selected.clear();
|
this.selected.clear();
|
||||||
this._container.querySelectorAll('.photo-tile.selected').forEach((t) => t.classList.remove('selected'));
|
this._container.querySelectorAll('.photo-tile.selected').forEach((t) => {
|
||||||
|
t.classList.remove('selected');
|
||||||
|
});
|
||||||
this._hideSelectionBar();
|
this._hideSelectionBar();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ const photosLightbox = {
|
|||||||
content.innerHTML = '<div class="photos-loading"><i class="fas fa-spinner"></i></div>';
|
content.innerHTML = '<div class="photos-loading"><i class="fas fa-spinner"></i></div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isVideo = item.mime_type && item.mime_type.startsWith('video/');
|
const isVideo = item.mime_type?.startsWith('video/');
|
||||||
const res = await fetch(`/api/files/${item.id}`, {
|
const res = await fetch(`/api/files/${item.id}`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: this._headers()
|
headers: this._headers()
|
||||||
@@ -172,7 +172,7 @@ const photosLightbox = {
|
|||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
let parts = [dateStr];
|
const parts = [dateStr];
|
||||||
if (sizeStr) parts.push(sizeStr);
|
if (sizeStr) parts.push(sizeStr);
|
||||||
if (data.camera_make || data.camera_model) {
|
if (data.camera_make || data.camera_model) {
|
||||||
parts.push([data.camera_make, data.camera_model].filter(Boolean).join(' '));
|
parts.push([data.camera_make, data.camera_model].filter(Boolean).join(' '));
|
||||||
@@ -182,7 +182,7 @@ const photosLightbox = {
|
|||||||
}
|
}
|
||||||
metaEl.textContent = parts.join(' · ');
|
metaEl.textContent = parts.join(' · ');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
// Non-critical, keep existing meta
|
// Non-critical, keep existing meta
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const recent = {
|
|||||||
*/
|
*/
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
document.addEventListener('file-accessed', (event) => {
|
document.addEventListener('file-accessed', (event) => {
|
||||||
if (event.detail && event.detail.file) {
|
if (event.detail?.file) {
|
||||||
const file = event.detail.file;
|
const file = event.detail.file;
|
||||||
const itemType = file.item_type || 'file';
|
const itemType = file.item_type || 'file';
|
||||||
this._recordAccess(file.id, itemType);
|
this._recordAccess(file.id, itemType);
|
||||||
@@ -144,7 +144,7 @@ const recent = {
|
|||||||
if (files.length) window.ui.renderFiles(files);
|
if (files.length) window.ui.renderFiles(files);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error displaying recent files:', error);
|
console.error('Error displaying recent files:', error);
|
||||||
if (window.ui && window.ui.showNotification) {
|
if (window.ui?.showNotification) {
|
||||||
window.ui.showNotification('Error', 'Error loading recent files');
|
window.ui.showNotification('Error', 'Error loading recent files');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ function t(key, params) {
|
|||||||
/** Escape a string for safe embedding inside a JS string literal within an HTML attribute. */
|
/** Escape a string for safe embedding inside a JS string literal within an HTML attribute. */
|
||||||
function _escJs(s) {
|
function _escJs(s) {
|
||||||
if (typeof s !== 'string') return '';
|
if (typeof s !== 'string') return '';
|
||||||
return s.replace(/[^\w .\-]/g, function (c) {
|
return s.replace(/[^\w .-]/g, (c) => {
|
||||||
return '\\x' + c.charCodeAt(0).toString(16).padStart(2, '0');
|
return `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ function formatBytes(bytes) {
|
|||||||
const k = 1024,
|
const k = 1024,
|
||||||
sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(dateStr) {
|
function timeAgo(dateStr) {
|
||||||
@@ -63,11 +63,11 @@ function timeAgo(dateStr) {
|
|||||||
|
|
||||||
/* ── Custom confirm modal ── */
|
/* ── Custom confirm modal ── */
|
||||||
function showConfirm(message) {
|
function showConfirm(message) {
|
||||||
return new Promise(function (resolve) {
|
return new Promise((resolve) => {
|
||||||
var overlay = document.getElementById('confirm-modal');
|
const overlay = document.getElementById('confirm-modal');
|
||||||
var msgEl = document.getElementById('confirm-message');
|
const msgEl = document.getElementById('confirm-message');
|
||||||
var yesBtn = document.getElementById('confirm-yes');
|
const yesBtn = document.getElementById('confirm-yes');
|
||||||
var noBtn = document.getElementById('confirm-cancel');
|
const noBtn = document.getElementById('confirm-cancel');
|
||||||
msgEl.textContent = message;
|
msgEl.textContent = message;
|
||||||
overlay.classList.remove('hidden');
|
overlay.classList.remove('hidden');
|
||||||
overlay.classList.add('show-flex');
|
overlay.classList.add('show-flex');
|
||||||
@@ -100,10 +100,10 @@ let activeTabName = 'dashboard';
|
|||||||
|
|
||||||
function switchTab(name, el) {
|
function switchTab(name, el) {
|
||||||
if (name === activeTabName) return;
|
if (name === activeTabName) return;
|
||||||
var oldTab = document.getElementById('tab-' + activeTabName);
|
var oldTab = document.getElementById(`tab-${activeTabName}`);
|
||||||
var newTab = document.getElementById('tab-' + name);
|
var newTab = document.getElementById(`tab-${name}`);
|
||||||
|
|
||||||
document.querySelectorAll('.admin-tab').forEach(function (b) {
|
document.querySelectorAll('.admin-tab').forEach((b) => {
|
||||||
b.classList.remove('active');
|
b.classList.remove('active');
|
||||||
});
|
});
|
||||||
if (el) el.classList.add('active');
|
if (el) el.classList.add('active');
|
||||||
@@ -138,7 +138,7 @@ function switchTab(name, el) {
|
|||||||
|
|
||||||
async function loadDashboard() {
|
async function loadDashboard() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/dashboard', {
|
const resp = await fetch(`${API}/admin/dashboard`, {
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
@@ -147,13 +147,13 @@ async function loadDashboard() {
|
|||||||
document.getElementById('ds-total-users').textContent = d.total_users;
|
document.getElementById('ds-total-users').textContent = d.total_users;
|
||||||
document.getElementById('ds-active-users').textContent = d.active_users;
|
document.getElementById('ds-active-users').textContent = d.active_users;
|
||||||
document.getElementById('ds-admin-users').textContent = d.admin_users;
|
document.getElementById('ds-admin-users').textContent = d.admin_users;
|
||||||
document.getElementById('ds-version').textContent = 'v' + d.server_version;
|
document.getElementById('ds-version').textContent = `v${d.server_version}`;
|
||||||
document.getElementById('ds-used').textContent = formatBytes(d.total_used_bytes);
|
document.getElementById('ds-used').textContent = formatBytes(d.total_used_bytes);
|
||||||
document.getElementById('ds-quota').textContent = formatBytes(d.total_quota_bytes);
|
document.getElementById('ds-quota').textContent = formatBytes(d.total_quota_bytes);
|
||||||
document.getElementById('ds-usage-pct').textContent = d.storage_usage_percent.toFixed(1) + '%';
|
document.getElementById('ds-usage-pct').textContent = `${d.storage_usage_percent.toFixed(1)}%`;
|
||||||
const bar = document.getElementById('ds-bar');
|
const bar = document.getElementById('ds-bar');
|
||||||
bar.style.width = Math.min(d.storage_usage_percent, 100) + '%';
|
bar.style.width = `${Math.min(d.storage_usage_percent, 100)}%`;
|
||||||
bar.className = 'progress-fill ' + (d.storage_usage_percent > 90 ? 'red' : d.storage_usage_percent > 70 ? 'orange' : 'green');
|
bar.className = `progress-fill ${d.storage_usage_percent > 90 ? 'red' : d.storage_usage_percent > 70 ? 'orange' : 'green'}`;
|
||||||
document.getElementById('ds-auth').textContent = d.auth_enabled ? t('admin.enabled') : t('admin.disabled');
|
document.getElementById('ds-auth').textContent = d.auth_enabled ? t('admin.enabled') : t('admin.disabled');
|
||||||
document.getElementById('ds-oidc').textContent = d.oidc_configured ? t('admin.active') : t('admin.off');
|
document.getElementById('ds-oidc').textContent = d.oidc_configured ? t('admin.active') : t('admin.off');
|
||||||
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? t('admin.enabled') : t('admin.disabled');
|
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? t('admin.enabled') : t('admin.disabled');
|
||||||
@@ -179,10 +179,9 @@ async function loadDashboard() {
|
|||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
const tbody = document.getElementById('users-tbody');
|
const tbody = document.getElementById('users-tbody');
|
||||||
tbody.innerHTML =
|
tbody.innerHTML = `<tr><td colspan="7" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.loading_users'))}</td></tr>`;
|
||||||
'<tr><td colspan="7" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('admin.loading_users')) + '</td></tr>';
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users?limit=' + PAGE_SIZE + '&offset=' + usersPage * PAGE_SIZE, {
|
const resp = await fetch(`${API}/admin/users?limit=${PAGE_SIZE}&offset=${usersPage * PAGE_SIZE}`, {
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
@@ -197,7 +196,7 @@ async function loadUsers() {
|
|||||||
totalUsers = data.total;
|
totalUsers = data.total;
|
||||||
const users = data.users;
|
const users = data.users;
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="7" class="table-status-empty">' + escapeHtml(t('admin.no_users_found')) + '</td></tr>';
|
tbody.innerHTML = `<tr><td colspan="7" class="table-status-empty">${escapeHtml(t('admin.no_users_found'))}</td></tr>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,8 +206,8 @@ async function loadUsers() {
|
|||||||
const quotaColor = quotaPct > 90 ? 'red' : quotaPct > 70 ? 'orange' : 'green';
|
const quotaColor = quotaPct > 90 ? 'red' : quotaPct > 70 ? 'orange' : 'green';
|
||||||
const quotaText =
|
const quotaText =
|
||||||
u.storage_quota_bytes > 0
|
u.storage_quota_bytes > 0
|
||||||
? formatBytes(u.storage_used_bytes) + ' / ' + formatBytes(u.storage_quota_bytes)
|
? `${formatBytes(u.storage_used_bytes)} / ${formatBytes(u.storage_quota_bytes)}`
|
||||||
: formatBytes(u.storage_used_bytes) + ' / ∞';
|
: `${formatBytes(u.storage_used_bytes)} / ∞`;
|
||||||
const isSelf = u.id === currentAdminId;
|
const isSelf = u.id === currentAdminId;
|
||||||
const isOidc = u.auth_provider && u.auth_provider !== 'local';
|
const isOidc = u.auth_provider && u.auth_provider !== 'local';
|
||||||
const authBadge = isOidc
|
const authBadge = isOidc
|
||||||
@@ -217,12 +216,12 @@ async function loadUsers() {
|
|||||||
'"><i class="fas fa-key badge-admin-icon-small"></i> ' +
|
'"><i class="fas fa-key badge-admin-icon-small"></i> ' +
|
||||||
escapeHtml(u.auth_provider) +
|
escapeHtml(u.auth_provider) +
|
||||||
'</span>'
|
'</span>'
|
||||||
: '<span class="badge badge-local">' + escapeHtml(t('admin.local')) + '</span>';
|
: `<span class="badge badge-local">${escapeHtml(t('admin.local'))}</span>`;
|
||||||
return (
|
return (
|
||||||
'<tr>' +
|
'<tr>' +
|
||||||
'<td><div class="user-info"><span class="user-name">' +
|
'<td><div class="user-info"><span class="user-name">' +
|
||||||
escapeHtml(u.username) +
|
escapeHtml(u.username) +
|
||||||
(isSelf ? ' <span class="user-self-badge">' + escapeHtml(t('admin.you_badge')) + '</span>' : '') +
|
(isSelf ? ` <span class="user-self-badge">${escapeHtml(t('admin.you_badge'))}</span>` : '') +
|
||||||
'</span><span class="user-email">' +
|
'</span><span class="user-email">' +
|
||||||
escapeHtml(u.email) +
|
escapeHtml(u.email) +
|
||||||
'</span></div></td>' +
|
'</span></div></td>' +
|
||||||
@@ -308,15 +307,15 @@ async function loadUsers() {
|
|||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
// Set dynamic progress bar widths (CSP-safe via JS property)
|
// Set dynamic progress bar widths (CSP-safe via JS property)
|
||||||
document.querySelectorAll('.progress-fill[data-width]').forEach(function (el) {
|
document.querySelectorAll('.progress-fill[data-width]').forEach((el) => {
|
||||||
el.style.width = el.dataset.width + '%';
|
el.style.width = `${el.dataset.width}%`;
|
||||||
el.removeAttribute('data-width');
|
el.removeAttribute('data-width');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wire up admin action buttons (replaces inline onclick handlers)
|
// Wire up admin action buttons (replaces inline onclick handlers)
|
||||||
document.querySelectorAll('.admin-action-btn').forEach(function (btn) {
|
document.querySelectorAll('.admin-action-btn').forEach((btn) => {
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', () => {
|
||||||
var action = btn.dataset.action;
|
const action = btn.dataset.action;
|
||||||
if (action === 'quota') openQuotaModal(btn.dataset.uid, btn.dataset.uname, Number(btn.dataset.quota));
|
if (action === 'quota') openQuotaModal(btn.dataset.uid, btn.dataset.uname, Number(btn.dataset.quota));
|
||||||
else if (action === 'reset-pw') openResetPasswordModal(btn.dataset.uid, btn.dataset.uname);
|
else if (action === 'reset-pw') openResetPasswordModal(btn.dataset.uid, btn.dataset.uname);
|
||||||
else if (action === 'toggle-role') toggleRole(btn.dataset.uid, btn.dataset.role);
|
else if (action === 'toggle-role') toggleRole(btn.dataset.uid, btn.dataset.role);
|
||||||
@@ -356,7 +355,7 @@ async function toggleRole(userId, currentRole) {
|
|||||||
const ok = await showConfirm(t('admin.confirm_role_change', { role: newRole }));
|
const ok = await showConfirm(t('admin.confirm_role_change', { role: newRole }));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users/' + userId + '/role', {
|
const resp = await fetch(`${API}/admin/users/${userId}/role`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -377,7 +376,7 @@ async function toggleActive(userId, currentActive) {
|
|||||||
const ok = await showConfirm(msg);
|
const ok = await showConfirm(msg);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users/' + userId + '/active', {
|
const resp = await fetch(`${API}/admin/users/${userId}/active`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -397,7 +396,7 @@ async function deleteUser(userId, username) {
|
|||||||
const ok = await showConfirm(t('admin.confirm_delete_user', { name: username }));
|
const ok = await showConfirm(t('admin.confirm_delete_user', { name: username }));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users/' + userId, {
|
const resp = await fetch(`${API}/admin/users/${userId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
@@ -429,10 +428,10 @@ function closeQuotaModal() {
|
|||||||
|
|
||||||
async function saveQuota() {
|
async function saveQuota() {
|
||||||
const val = parseFloat(document.getElementById('qm-value').value) || 0;
|
const val = parseFloat(document.getElementById('qm-value').value) || 0;
|
||||||
const unit = parseInt(document.getElementById('qm-unit').value);
|
const unit = parseInt(document.getElementById('qm-unit').value, 10);
|
||||||
const bytes = Math.round(val * unit);
|
const bytes = Math.round(val * unit);
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users/' + quotaUserId + '/quota', {
|
const resp = await fetch(`${API}/admin/users/${quotaUserId}/quota`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -473,7 +472,7 @@ async function submitCreateUser() {
|
|||||||
const email = document.getElementById('cu-email').value.trim() || null;
|
const email = document.getElementById('cu-email').value.trim() || null;
|
||||||
const role = document.getElementById('cu-role').value;
|
const role = document.getElementById('cu-role').value;
|
||||||
const quotaVal = parseFloat(document.getElementById('cu-quota-value').value) || 0;
|
const quotaVal = parseFloat(document.getElementById('cu-quota-value').value) || 0;
|
||||||
const quotaUnit = parseInt(document.getElementById('cu-quota-unit').value);
|
const quotaUnit = parseInt(document.getElementById('cu-quota-unit').value, 10);
|
||||||
const quotaBytes = Math.round(quotaVal * quotaUnit);
|
const quotaBytes = Math.round(quotaVal * quotaUnit);
|
||||||
|
|
||||||
const errorEl = document.getElementById('cu-error');
|
const errorEl = document.getElementById('cu-error');
|
||||||
@@ -490,9 +489,9 @@ async function submitCreateUser() {
|
|||||||
|
|
||||||
const btn = document.getElementById('cu-submit');
|
const btn = document.getElementById('cu-submit');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('admin.creating'));
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.creating'))}`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users', {
|
const resp = await fetch(`${API}/admin/users`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -518,7 +517,7 @@ async function submitCreateUser() {
|
|||||||
errorEl.className = 'alert alert-error';
|
errorEl.className = 'alert alert-error';
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="fas fa-user-plus"></i> ' + escapeHtml(t('admin.create_user'));
|
btn.innerHTML = `<i class="fas fa-user-plus"></i> ${escapeHtml(t('admin.create_user'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let resetPwUserId = '';
|
let resetPwUserId = '';
|
||||||
@@ -546,9 +545,9 @@ async function submitResetPassword() {
|
|||||||
|
|
||||||
const btn = document.getElementById('rp-submit');
|
const btn = document.getElementById('rp-submit');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('admin.resetting'));
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.resetting'))}`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/users/' + resetPwUserId + '/password', {
|
const resp = await fetch(`${API}/admin/users/${resetPwUserId}/password`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -566,14 +565,14 @@ async function submitResetPassword() {
|
|||||||
errorEl.className = 'alert alert-error';
|
errorEl.className = 'alert alert-error';
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('admin.reset_btn'));
|
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('admin.reset_btn'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleRegistration(enabled) {
|
async function toggleRegistration(enabled) {
|
||||||
if (enabled) hideElement('registration-warning');
|
if (enabled) hideElement('registration-warning');
|
||||||
else showElement('registration-warning', 'flex');
|
else showElement('registration-warning', 'flex');
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/settings/registration', {
|
const resp = await fetch(`${API}/admin/settings/registration`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -606,7 +605,7 @@ document.getElementById('disable-password').addEventListener('change', function
|
|||||||
function showOidcStatus(msg, type) {
|
function showOidcStatus(msg, type) {
|
||||||
const el = document.getElementById('oidc-status');
|
const el = document.getElementById('oidc-status');
|
||||||
el.textContent = msg;
|
el.textContent = msg;
|
||||||
el.className = 'alert alert-' + type;
|
el.className = `alert alert-${type}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyCallback() {
|
function copyCallback() {
|
||||||
@@ -622,10 +621,10 @@ async function testConnection() {
|
|||||||
}
|
}
|
||||||
const btn = document.getElementById('discover-btn');
|
const btn = document.getElementById('discover-btn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('admin.discovering'));
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.discovering'))}`;
|
||||||
const resultDiv = document.getElementById('discovery-result');
|
const resultDiv = document.getElementById('discovery-result');
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/settings/oidc/test', {
|
const resp = await fetch(`${API}/admin/settings/oidc/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -644,19 +643,19 @@ async function testConnection() {
|
|||||||
if (!document.getElementById('provider-name').value && r.provider_name_suggestion)
|
if (!document.getElementById('provider-name').value && r.provider_name_suggestion)
|
||||||
document.getElementById('provider-name').value = r.provider_name_suggestion;
|
document.getElementById('provider-name').value = r.provider_name_suggestion;
|
||||||
} else {
|
} else {
|
||||||
resultDiv.innerHTML = '<div class="discovery-result fail"><strong><i class="fas fa-times-circle"></i> ' + escapeHtml(r.message) + '</strong></div>';
|
resultDiv.innerHTML = `<div class="discovery-result fail"><strong><i class="fas fa-times-circle"></i> ${escapeHtml(r.message)}</strong></div>`;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
resultDiv.innerHTML = '<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ' + escapeHtml(e.message) + '</div>';
|
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="fas fa-search"></i> ' + escapeHtml(t('admin.auto_discover'));
|
btn.innerHTML = `<i class="fas fa-search"></i> ${escapeHtml(t('admin.auto_discover'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveOidcSettings() {
|
async function saveOidcSettings() {
|
||||||
const btn = document.getElementById('save-btn');
|
const btn = document.getElementById('save-btn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('admin.saving'));
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.saving'))}`;
|
||||||
const body = {
|
const body = {
|
||||||
enabled: document.getElementById('oidc-enabled').checked,
|
enabled: document.getElementById('oidc-enabled').checked,
|
||||||
issuer_url: document.getElementById('issuer-url').value.trim(),
|
issuer_url: document.getElementById('issuer-url').value.trim(),
|
||||||
@@ -669,7 +668,7 @@ async function saveOidcSettings() {
|
|||||||
provider_name: document.getElementById('provider-name').value.trim() || null
|
provider_name: document.getElementById('provider-name').value.trim() || null
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/admin/settings/oidc', {
|
const resp = await fetch(`${API}/admin/settings/oidc`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -681,18 +680,18 @@ async function saveOidcSettings() {
|
|||||||
loadDashboard();
|
loadDashboard();
|
||||||
} else {
|
} else {
|
||||||
const e = await resp.json().catch(() => ({}));
|
const e = await resp.json().catch(() => ({}));
|
||||||
showOidcStatus('Error: ' + (e.message || resp.statusText), 'error');
|
showOidcStatus(`Error: ${e.message || resp.statusText}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showOidcStatus(t('admin.error_network', { message: e.message }), 'error');
|
showOidcStatus(t('admin.error_network', { message: e.message }), 'error');
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('admin.save_btn'));
|
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('admin.save_btn'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
const me = await fetch(API + '/auth/me', {
|
const me = await fetch(`${API}/auth/me`, {
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
@@ -707,7 +706,7 @@ async function init() {
|
|||||||
}
|
}
|
||||||
currentAdminId = user.id;
|
currentAdminId = user.id;
|
||||||
|
|
||||||
const oidcResp = await fetch(API + '/admin/settings/oidc', {
|
const oidcResp = await fetch(`${API}/admin/settings/oidc`, {
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
@@ -728,7 +727,7 @@ async function init() {
|
|||||||
document.getElementById('callback-url').textContent = s.callback_url;
|
document.getElementById('callback-url').textContent = s.callback_url;
|
||||||
if (s.client_secret_set) showElement('secret-hint');
|
if (s.client_secret_set) showElement('secret-hint');
|
||||||
(s.env_overrides || []).forEach((field) => {
|
(s.env_overrides || []).forEach((field) => {
|
||||||
const badge = document.getElementById('badge-' + field);
|
const badge = document.getElementById(`badge-${field}`);
|
||||||
if (badge) badge.innerHTML = '<span class="badge badge-env">ENV</span>';
|
if (badge) badge.innerHTML = '<span class="badge badge-env">ENV</span>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -748,14 +747,14 @@ function showAccessDenied() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Apply i18n when translations load / change ── */
|
/* ── Apply i18n when translations load / change ── */
|
||||||
document.addEventListener('translationsLoaded', function () {
|
document.addEventListener('translationsLoaded', () => {
|
||||||
if (window.i18n && window.i18n.translatePage) window.i18n.translatePage();
|
if (window.i18n?.translatePage) window.i18n.translatePage();
|
||||||
// Re-render dynamic content that uses t()
|
// Re-render dynamic content that uses t()
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
if (activeTabName === 'users') loadUsers();
|
if (activeTabName === 'users') loadUsers();
|
||||||
});
|
});
|
||||||
document.addEventListener('localeChanged', function () {
|
document.addEventListener('localeChanged', () => {
|
||||||
if (window.i18n && window.i18n.translatePage) window.i18n.translatePage();
|
if (window.i18n?.translatePage) window.i18n.translatePage();
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
if (activeTabName === 'users') loadUsers();
|
if (activeTabName === 'users') loadUsers();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
// device-verify.js — Extracted from inline <script> in device-verify.html
|
// device-verify.js — Extracted from inline <script> in device-verify.html
|
||||||
(function () {
|
(() => {
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var API_BASE = window.location.origin;
|
var API_BASE = window.location.origin;
|
||||||
var codeInput = document.getElementById('user-code');
|
var codeInput = document.getElementById('user-code');
|
||||||
var deviceInfo = document.getElementById('device-info');
|
var deviceInfo = document.getElementById('device-info');
|
||||||
@@ -20,11 +18,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto-insert hyphen and lookup on input
|
// Auto-insert hyphen and lookup on input
|
||||||
codeInput.addEventListener('input', function (e) {
|
codeInput.addEventListener('input', (e) => {
|
||||||
var val = e.target.value.toUpperCase().replace(/[^A-Z0-9\-]/g, '');
|
var val = e.target.value.toUpperCase().replace(/[^A-Z0-9-]/g, '');
|
||||||
// Auto-insert hyphen after 4 chars
|
// Auto-insert hyphen after 4 chars
|
||||||
if (val.length === 4 && val.indexOf('-') === -1) {
|
if (val.length === 4 && val.indexOf('-') === -1) {
|
||||||
val = val + '-';
|
val = `${val}-`;
|
||||||
}
|
}
|
||||||
e.target.value = val;
|
e.target.value = val;
|
||||||
errorText.classList.add('hidden');
|
errorText.classList.add('hidden');
|
||||||
@@ -32,7 +30,7 @@
|
|||||||
// Debounce lookup
|
// Debounce lookup
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
if (val.length >= 9) {
|
if (val.length >= 9) {
|
||||||
debounceTimer = setTimeout(function () {
|
debounceTimer = setTimeout(() => {
|
||||||
lookupCode(val);
|
lookupCode(val);
|
||||||
}, 300);
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
@@ -42,16 +40,16 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Wire up approve / deny buttons (replaces inline onclick)
|
// Wire up approve / deny buttons (replaces inline onclick)
|
||||||
btnApprove.addEventListener('click', function () {
|
btnApprove.addEventListener('click', () => {
|
||||||
handleAction('approve');
|
handleAction('approve');
|
||||||
});
|
});
|
||||||
btnDeny.addEventListener('click', function () {
|
btnDeny.addEventListener('click', () => {
|
||||||
handleAction('deny');
|
handleAction('deny');
|
||||||
});
|
});
|
||||||
|
|
||||||
async function lookupCode(code) {
|
async function lookupCode(code) {
|
||||||
try {
|
try {
|
||||||
var resp = await fetch(API_BASE + '/api/auth/device/verify?code=' + encodeURIComponent(code), {
|
const resp = await fetch(`${API_BASE}/api/auth/device/verify?code=${encodeURIComponent(code)}`, {
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
if (resp.status === 401) {
|
if (resp.status === 401) {
|
||||||
@@ -59,7 +57,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!resp.ok) throw new Error('Lookup failed');
|
if (!resp.ok) throw new Error('Lookup failed');
|
||||||
var data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
if (data.valid) {
|
if (data.valid) {
|
||||||
currentCode = code;
|
currentCode = code;
|
||||||
@@ -83,7 +81,7 @@
|
|||||||
btnDeny.disabled = true;
|
btnDeny.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var resp = await fetch(API_BASE + '/api/auth/device/verify', {
|
const resp = await fetch(`${API_BASE}/api/auth/device/verify`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: Object.assign({ 'Content-Type': 'application/json' }, getCsrfHeaders()),
|
headers: Object.assign({ 'Content-Type': 'application/json' }, getCsrfHeaders()),
|
||||||
@@ -91,7 +89,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
var err = await resp.json().catch(function () {
|
const err = await resp.json().catch(() => {
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
throw new Error(err.message || 'Action failed');
|
throw new Error(err.message || 'Action failed');
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ switch (errorType) {
|
|||||||
errorTitle.textContent = 'Login Failed';
|
errorTitle.textContent = 'Login Failed';
|
||||||
errorMessage.textContent = 'Invalid username or password. Please check your credentials and try again.';
|
errorMessage.textContent = 'Invalid username or password. Please check your credentials and try again.';
|
||||||
errorAction.textContent = 'Try Again';
|
errorAction.textContent = 'Try Again';
|
||||||
errorAction.addEventListener('click', function () {
|
errorAction.addEventListener('click', () => {
|
||||||
history.back();
|
history.back();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -19,7 +19,7 @@ switch (errorType) {
|
|||||||
errorTitle.textContent = 'Session Expired';
|
errorTitle.textContent = 'Session Expired';
|
||||||
errorMessage.textContent = 'Your session has expired. Please try again.';
|
errorMessage.textContent = 'Your session has expired. Please try again.';
|
||||||
errorAction.textContent = 'Close Window';
|
errorAction.textContent = 'Close Window';
|
||||||
errorAction.addEventListener('click', function () {
|
errorAction.addEventListener('click', () => {
|
||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -27,7 +27,7 @@ switch (errorType) {
|
|||||||
errorTitle.textContent = 'Not Found';
|
errorTitle.textContent = 'Not Found';
|
||||||
errorMessage.textContent = 'The requested page was not found.';
|
errorMessage.textContent = 'The requested page was not found.';
|
||||||
errorAction.textContent = 'Close Window';
|
errorAction.textContent = 'Close Window';
|
||||||
errorAction.addEventListener('click', function () {
|
errorAction.addEventListener('click', () => {
|
||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -35,7 +35,7 @@ switch (errorType) {
|
|||||||
errorTitle.textContent = 'Error';
|
errorTitle.textContent = 'Error';
|
||||||
errorMessage.textContent = 'An unexpected error occurred. Please try again.';
|
errorMessage.textContent = 'An unexpected error occurred. Please try again.';
|
||||||
errorAction.textContent = 'Close Window';
|
errorAction.textContent = 'Close Window';
|
||||||
errorAction.addEventListener('click', function () {
|
errorAction.addEventListener('click', () => {
|
||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,22 +6,22 @@ if (!/^[0-9a-fA-F]+$/.test(token)) {
|
|||||||
document.body.innerHTML = '<p>Invalid session token.</p>';
|
document.body.innerHTML = '<p>Invalid session token.</p>';
|
||||||
throw new Error('Invalid token format');
|
throw new Error('Invalid token format');
|
||||||
}
|
}
|
||||||
document.getElementById('login-flow-form').action = '/login/v2/flow/' + token;
|
document.getElementById('login-flow-form').action = `/login/v2/flow/${token}`;
|
||||||
|
|
||||||
// Check if OIDC is available and configure SSO button
|
// Check if OIDC is available and configure SSO button
|
||||||
(async function () {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
var resp = await fetch('/api/auth/oidc/providers');
|
const resp = await fetch('/api/auth/oidc/providers');
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
var info = await resp.json();
|
const info = await resp.json();
|
||||||
if (!info.enabled) return;
|
if (!info.enabled) return;
|
||||||
|
|
||||||
// Show OIDC section
|
// Show OIDC section
|
||||||
document.getElementById('oidc-section').classList.remove('hidden');
|
document.getElementById('oidc-section').classList.remove('hidden');
|
||||||
|
|
||||||
// Update button text with provider name
|
// Update button text with provider name
|
||||||
var btn = document.getElementById('oidc-button');
|
const btn = document.getElementById('oidc-button');
|
||||||
btn.textContent = 'Sign in with ' + (info.provider_name || 'SSO');
|
btn.textContent = `Sign in with ${info.provider_name || 'SSO'}`;
|
||||||
|
|
||||||
// If password login is disabled, hide the password form
|
// If password login is disabled, hide the password form
|
||||||
if (!info.password_login_enabled) {
|
if (!info.password_login_enabled) {
|
||||||
@@ -29,10 +29,10 @@ document.getElementById('login-flow-form').action = '/login/v2/flow/' + token;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SSO button redirects to the OIDC flow for this NC token
|
// SSO button redirects to the OIDC flow for this NC token
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', () => {
|
||||||
window.location.href = '/login/v2/flow/' + token + '/oidc';
|
window.location.href = `/login/v2/flow/${token}/oidc`;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
// OIDC not available — silently keep password-only mode
|
// OIDC not available — silently keep password-only mode
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
document.getElementById('close-window-btn').addEventListener('click', function () {
|
document.getElementById('close-window-btn').addEventListener('click', () => {
|
||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
// Auto-close after 3 seconds
|
// Auto-close after 3 seconds
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
window.close();
|
window.close();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function formatBytes(bytes) {
|
|||||||
const k = 1024,
|
const k = 1024,
|
||||||
sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(dateStr) {
|
function timeAgo(dateStr) {
|
||||||
@@ -32,7 +32,7 @@ function timeAgo(dateStr) {
|
|||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/auth/me', {
|
const resp = await fetch(`${API}/auth/me`, {
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
@@ -50,10 +50,10 @@ async function init() {
|
|||||||
const badge = document.getElementById('p-role-badge');
|
const badge = document.getElementById('p-role-badge');
|
||||||
if (user.role === 'admin') {
|
if (user.role === 'admin') {
|
||||||
badge.className = 'role-badge role-badge-admin';
|
badge.className = 'role-badge role-badge-admin';
|
||||||
badge.innerHTML = '<i class="fas fa-shield-alt"></i> ' + t('profile.role_admin');
|
badge.innerHTML = `<i class="fas fa-shield-alt"></i> ${t('profile.role_admin')}`;
|
||||||
} else {
|
} else {
|
||||||
badge.className = 'role-badge role-badge-user';
|
badge.className = 'role-badge role-badge-user';
|
||||||
badge.innerHTML = '<i class="fas fa-user"></i> ' + t('profile.role_user');
|
badge.innerHTML = `<i class="fas fa-user"></i> ${t('profile.role_user')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('p-detail-username').textContent = user.username;
|
document.getElementById('p-detail-username').textContent = user.username;
|
||||||
@@ -67,12 +67,12 @@ async function init() {
|
|||||||
|
|
||||||
document.getElementById('p-storage-used').textContent = formatBytes(used);
|
document.getElementById('p-storage-used').textContent = formatBytes(used);
|
||||||
document.getElementById('p-storage-quota').textContent = quota > 0 ? formatBytes(quota) : '∞';
|
document.getElementById('p-storage-quota').textContent = quota > 0 ? formatBytes(quota) : '∞';
|
||||||
document.getElementById('p-storage-pct').textContent = quota > 0 ? pct + '%' : '—';
|
document.getElementById('p-storage-pct').textContent = quota > 0 ? `${pct}%` : '—';
|
||||||
|
|
||||||
const bar = document.getElementById('p-storage-bar');
|
const bar = document.getElementById('p-storage-bar');
|
||||||
bar.style.width = pct + '%';
|
bar.style.width = `${pct}%`;
|
||||||
bar.className = 'storage-fill ' + (pct > 90 ? 'red' : pct > 70 ? 'orange' : 'green');
|
bar.className = `storage-fill ${pct > 90 ? 'red' : pct > 70 ? 'orange' : 'green'}`;
|
||||||
document.getElementById('p-storage-text').textContent = formatBytes(used) + ' / ' + (quota > 0 ? formatBytes(quota) : t('profile.unlimited'));
|
document.getElementById('p-storage-text').textContent = `${formatBytes(used)} / ${quota > 0 ? formatBytes(quota) : t('profile.unlimited')}`;
|
||||||
|
|
||||||
if (user.auth_provider && user.auth_provider !== 'local') {
|
if (user.auth_provider && user.auth_provider !== 'local') {
|
||||||
document.getElementById('password-section').classList.add('hidden');
|
document.getElementById('password-section').classList.add('hidden');
|
||||||
@@ -81,7 +81,7 @@ async function init() {
|
|||||||
loadAppPasswords();
|
loadAppPasswords();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const oidcResp = await fetch(API + '/auth/oidc/providers', {
|
const oidcResp = await fetch(`${API}/auth/oidc/providers`, {
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
if (oidcResp.ok) {
|
if (oidcResp.ok) {
|
||||||
@@ -90,7 +90,7 @@ async function init() {
|
|||||||
document.getElementById('password-section').classList.add('hidden');
|
document.getElementById('password-section').classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (oidcErr) {}
|
} catch (_oidcErr) {}
|
||||||
|
|
||||||
document.getElementById('loading').classList.add('hidden');
|
document.getElementById('loading').classList.add('hidden');
|
||||||
document.getElementById('main-content').classList.remove('hidden');
|
document.getElementById('main-content').classList.remove('hidden');
|
||||||
@@ -113,23 +113,21 @@ async function changePassword(e) {
|
|||||||
const statusEl = document.getElementById('pw-status');
|
const statusEl = document.getElementById('pw-status');
|
||||||
|
|
||||||
if (newPw !== confirmPw) {
|
if (newPw !== confirmPw) {
|
||||||
statusEl.innerHTML =
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.passwords_no_match'))}</div>`;
|
||||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.passwords_no_match')) + '</div>';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPw.length < 8) {
|
if (newPw.length < 8) {
|
||||||
statusEl.innerHTML =
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.password_too_short'))}</div>`;
|
||||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.password_too_short')) + '</div>';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('pw-submit');
|
const btn = document.getElementById('pw-submit');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.updating'));
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('profile.updating'))}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/auth/change-password', {
|
const resp = await fetch(`${API}/auth/change-password`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -140,7 +138,7 @@ async function changePassword(e) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
statusEl.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle"></i> ' + escapeHtml(t('profile.password_updated')) + '</div>';
|
statusEl.innerHTML = `<div class="alert alert-success"><i class="fas fa-check-circle"></i> ${escapeHtml(t('profile.password_updated'))}</div>`;
|
||||||
document.getElementById('password-form').reset();
|
document.getElementById('password-form').reset();
|
||||||
} else {
|
} else {
|
||||||
const err = await resp.json().catch(() => ({}));
|
const err = await resp.json().catch(() => ({}));
|
||||||
@@ -157,7 +155,7 @@ async function changePassword(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('profile.update_password'));
|
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('profile.update_password'))}`;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +191,7 @@ function renderPwRow(pw) {
|
|||||||
btn.className = 'btn btn-danger-sm';
|
btn.className = 'btn btn-danger-sm';
|
||||||
btn.innerHTML = '<i class="fas fa-trash"></i>';
|
btn.innerHTML = '<i class="fas fa-trash"></i>';
|
||||||
btn.title = t('profile.revoke_title');
|
btn.title = t('profile.revoke_title');
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', () => {
|
||||||
revokeAppPassword(pw.id, pw.label);
|
revokeAppPassword(pw.id, pw.label);
|
||||||
});
|
});
|
||||||
actions.appendChild(btn);
|
actions.appendChild(btn);
|
||||||
@@ -204,7 +202,7 @@ function renderPwRow(pw) {
|
|||||||
|
|
||||||
async function loadAppPasswords() {
|
async function loadAppPasswords() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/auth/app-passwords', {
|
const resp = await fetch(`${API}/auth/app-passwords`, {
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
@@ -214,7 +212,7 @@ async function loadAppPasswords() {
|
|||||||
}
|
}
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
const passwords = data.app_passwords || data;
|
const passwords = data.app_passwords || data;
|
||||||
const userPws = passwords.filter(function (pw) {
|
const userPws = passwords.filter((pw) => {
|
||||||
return !isAutoPassword(pw);
|
return !isAutoPassword(pw);
|
||||||
});
|
});
|
||||||
const autoPws = passwords.filter(isAutoPassword);
|
const autoPws = passwords.filter(isAutoPassword);
|
||||||
@@ -264,17 +262,16 @@ async function createAppPassword() {
|
|||||||
const btn = document.getElementById('app-pw-generate');
|
const btn = document.getElementById('app-pw-generate');
|
||||||
|
|
||||||
if (!label) {
|
if (!label) {
|
||||||
statusEl.innerHTML =
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.error_label_required'))}</div>`;
|
||||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + escapeHtml(t('profile.error_label_required')) + '</div>';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + escapeHtml(t('profile.generating'));
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('profile.generating'))}`;
|
||||||
statusEl.innerHTML = '';
|
statusEl.innerHTML = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/auth/app-passwords', {
|
const resp = await fetch(`${API}/auth/app-passwords`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -295,19 +292,19 @@ async function createAppPassword() {
|
|||||||
labelInput.value = '';
|
labelInput.value = '';
|
||||||
loadAppPasswords();
|
loadAppPasswords();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.innerHTML = '<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' + err.message + '</div>';
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${err.message}</div>`;
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="fas fa-plus"></i> ' + escapeHtml(t('profile.generate'));
|
btn.innerHTML = `<i class="fas fa-plus"></i> ${escapeHtml(t('profile.generate'))}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyAppPassword() {
|
function copyAppPassword() {
|
||||||
const pw = document.getElementById('app-pw-created-password').textContent;
|
const pw = document.getElementById('app-pw-created-password').textContent;
|
||||||
navigator.clipboard.writeText(pw).then(function () {
|
navigator.clipboard.writeText(pw).then(() => {
|
||||||
const btn = document.getElementById('app-pw-copy-btn');
|
const btn = document.getElementById('app-pw-copy-btn');
|
||||||
btn.innerHTML = '<i class="fas fa-check"></i>';
|
btn.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
btn.innerHTML = '<i class="fas fa-copy"></i>';
|
btn.innerHTML = '<i class="fas fa-copy"></i>';
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
@@ -316,7 +313,7 @@ function copyAppPassword() {
|
|||||||
async function revokeAppPassword(id, label) {
|
async function revokeAppPassword(id, label) {
|
||||||
if (!confirm(t('profile.confirm_revoke', { label: label }))) return;
|
if (!confirm(t('profile.confirm_revoke', { label: label }))) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API + '/auth/app-passwords/' + encodeURIComponent(id), {
|
const resp = await fetch(`${API}/auth/app-passwords/${encodeURIComponent(id)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
@@ -348,9 +345,9 @@ document.getElementById('app-pw-copy-btn').addEventListener('click', copyAppPass
|
|||||||
document.getElementById('app-pw-auto-toggle').addEventListener('click', toggleAutoPasswords);
|
document.getElementById('app-pw-auto-toggle').addEventListener('click', toggleAutoPasswords);
|
||||||
|
|
||||||
/* Re-render when language changes */
|
/* Re-render when language changes */
|
||||||
window.addEventListener('translationsLoaded', function () {
|
window.addEventListener('translationsLoaded', () => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
window.addEventListener('localeChanged', function () {
|
window.addEventListener('localeChanged', () => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ const sharedView = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (window.i18n && window.i18n.translateElement) {
|
if (window.i18n?.translateElement) {
|
||||||
window.i18n.translateElement(container);
|
window.i18n.translateElement(container);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -271,7 +271,9 @@ const sharedView = {
|
|||||||
option.addEventListener('click', (e) => {
|
option.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Update active state
|
// Update active state
|
||||||
dropdown.querySelectorAll('.shared-select-option').forEach((o) => o.classList.remove('active'));
|
dropdown.querySelectorAll('.shared-select-option').forEach((o) => {
|
||||||
|
o.classList.remove('active');
|
||||||
|
});
|
||||||
option.classList.add('active');
|
option.classList.add('active');
|
||||||
// Update label
|
// Update label
|
||||||
const label = toggle.querySelector('.shared-select-label');
|
const label = toggle.querySelector('.shared-select-label');
|
||||||
@@ -529,8 +531,8 @@ const sharedView = {
|
|||||||
write: permWrite ? permWrite.checked : false,
|
write: permWrite ? permWrite.checked : false,
|
||||||
reshare: permReshare ? permReshare.checked : false
|
reshare: permReshare ? permReshare.checked : false
|
||||||
},
|
},
|
||||||
password: enablePw && enablePw.checked && pwField && pwField.value ? pwField.value : null,
|
password: enablePw?.checked && pwField?.value ? pwField.value : null,
|
||||||
expires_at: enableExp && enableExp.checked && expField && expField.value ? Math.floor(new Date(expField.value).getTime() / 1000) : null
|
expires_at: enableExp?.checked && expField?.value ? Math.floor(new Date(expField.value).getTime() / 1000) : null
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -588,7 +590,7 @@ const sharedView = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.fileSharing && window.fileSharing.sendShareNotification) {
|
if (window.fileSharing?.sendShareNotification) {
|
||||||
window.fileSharing
|
window.fileSharing
|
||||||
.sendShareNotification(this.currentItem.url, email, message)
|
.sendShareNotification(this.currentItem.url, email, message)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -600,7 +602,7 @@ const sharedView = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
showNotification(message, type = 'success') {
|
showNotification(message, type = 'success') {
|
||||||
if (window.ui && window.ui.showNotification) {
|
if (window.ui?.showNotification) {
|
||||||
window.ui.showNotification(message, type);
|
window.ui.showNotification(message, type);
|
||||||
} else {
|
} else {
|
||||||
alert(message);
|
alert(message);
|
||||||
@@ -616,7 +618,7 @@ const sharedView = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
translate(key, defaultText) {
|
translate(key, defaultText) {
|
||||||
if (window.i18n && window.i18n.t) return window.i18n.t(key, defaultText);
|
if (window.i18n?.t) return window.i18n.t(key, defaultText);
|
||||||
return defaultText;
|
return defaultText;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user