Compare commits

..

4 Commits

10 changed files with 359 additions and 125 deletions

View File

@@ -1,6 +1,69 @@
# Changelog # Changelog
## Changes 5/19/2025 ## Changes 5/23/2025 v1.3.8
- **Folder-strip context menu**
- Enabled right-click on items in the new folder strip (above file list) to open the same “Create / Rename / Share / Delete Folder” menu as in the main folder tree.
- Bound `contextmenu` event on each `.folder-item` in `loadFileList` to:
- Prevent the default browser menu
- Highlight the clicked folder-strip item
- Invoke `showFolderManagerContextMenu` with menu entries:
- Create Folder
- Rename Folder
- Share Folder (passes the strips `data-folder` value)
- Delete Folder
- Ensured menu actions are wrapped in arrow functions (`() => …`) so they fire only on menu-item click, not on render.
- Refactored folder-strip injection in `fileListView.js` to:
- Mark each strip item as `draggable="true"` (for drag-and-drop)
- Add `el.addEventListener("contextmenu", …)` alongside existing click/drag handlers
- Clean up global click listener for hiding the context menu
- Prevented premature invocation of `openFolderShareModal` by switching to `action: () => openFolderShareModal(dest)` instead of calling it directly.
- **Create File/Folder dropdown**
- Replaced standalone “Create File” button with a combined dropdown button in the actions toolbar.
- New markup
- Wired up JS handlers in `fileActions.js`:
- `#createFileOption``openCreateFileModal()`
- `#createFolderOption``document.getElementById('createFolderModal').style.display = 'block'`
- Toggled `.dropdown-menu` visibility on button click, and closed on outside click.
- Applied dark-mode support: dropdown background and text colors switch with `.dark-mode` class.
---
## Changes 5/22/2025 v1.3.7
- `.folder-strip-container .folder-name` css added to center text below folder material icon.
- Override file share_url to always use current origin
- Update `fileList` css to keep file name wrapping tight.
---
## Changes 5/21/2025
- **Drag & Drop to Folder Strip**
- Enabled dragging files from the file list directly onto the folder-strip items.
- Hooked up `folderDragOverHandler`, `folderDragLeaveHandler`, and `folderDropHandler` to `.folder-strip-container .folder-item`.
- On drop, files are moved via `/api/file/moveFiles.php` and the file list is refreshed.
- **Restore files from trash Toast Message**
- Changed the restore handlers so that the toast always reports the actual file(s) restored (e.g. “Restored file: foo.txt”) instead of “No trash record found.”
- Removed reliance on backend message payload and now generate the confirmation text client-side based on selected items.
---
## Changes 5/20/2025 v1.3.6
- **domUtils.js**
- `updateFileActionButtons`
- Hide selection buttons (`Delete Files`, `Copy Files`, `Move Files` & `Download ZIP`) until file is selected.
- Hide `Extract ZIP` until selecting zip files
- Hide `Create File` button when file list items are selected.
---
## Changes 5/19/2025 v1.3.5
### Added Folder strip & Create File ### Added Folder strip & Create File

View File

@@ -848,11 +848,27 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
background-color: #00796B; background-color: #00796B;
} }
#createFileBtn { #createBtn {
background-color: #007bff; background-color: #007bff;
color: white; color: white;
} }
body.dark-mode .dropdown-menu {
background-color: #2c2c2c !important;
border-color: #444 !important;
color: #e0e0e0!important;
}
body.dark-mode .dropdown-menu .dropdown-item {
color: #e0e0e0 !important;
}
.dropdown-item:hover {
background-color: rgba(0,0,0,0.05);
}
body.dark-mode .dropdown-item:hover {
background-color: rgba(255,255,255,0.1);
}
#fileList button.edit-btn { #fileList button.edit-btn {
background-color: #007bff; background-color: #007bff;
color: white; color: white;
@@ -971,16 +987,15 @@ body.dark-mode #fileList table tr {
} }
:root { :root {
--file-row-height: 48px; /* default, will be overwritten by your slider */ --file-row-height: 48px;
} }
/* Force each <tr> to be exactly the var() height */
#fileList table.table tbody tr { #fileList table.table tbody tr {
height: var(--file-row-height) !important; height: auto !important;
min-height: var(--file-row-height) !important;
} }
/* And force each <td> to match, with no extra padding or line-height */ #fileList table.table tbody td:not(.file-name-cell) {
#fileList table.table tbody td {
height: var(--file-row-height) !important; height: var(--file-row-height) !important;
line-height: var(--file-row-height) !important; line-height: var(--file-row-height) !important;
padding-top: 0 !important; padding-top: 0 !important;
@@ -988,6 +1003,13 @@ body.dark-mode #fileList table tr {
vertical-align: middle; vertical-align: middle;
} }
#fileList table.table tbody td.file-name-cell {
white-space: normal;
word-break: break-word;
line-height: 1.2em !important;
height: auto !important;
}
/* =========================================================== /* ===========================================================
HEADINGS & FORM LABELS HEADINGS & FORM LABELS
=========================================================== */ =========================================================== */
@@ -2261,13 +2283,20 @@ body.dark-mode .user-dropdown .user-menu .item:hover {
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
width: 80px; width: 80px;
color: inherit; /* icon will pick up text color */ color: inherit;
font-size: 0.85em; font-size: 0.85em;
} }
.folder-strip-container .folder-item i.material-icons { .folder-strip-container .folder-item i.material-icons {
font-size: 28px; font-size: 28px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.folder-strip-container .folder-name {
text-align: center;
white-space: normal;
word-break: break-word;
max-width: 80px;
margin-top: 4px;
}
.folder-strip-container .folder-item i.material-icons { .folder-strip-container .folder-item i.material-icons {
color: currentColor; color: currentColor;

View File

@@ -389,11 +389,38 @@
</div> </div>
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled <button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled
data-i18n-key="download_zip">Download ZIP</button> data-i18n-key="download_zip">Download ZIP</button>
<button id="extractZipBtn" class="btn btn-sm btn-info" data-i18n-title="extract_zip" <button id="extractZipBtn" class="btn action-btn btn-sm btn-info" data-i18n-title="extract_zip"
data-i18n-key="extract_zip_button">Extract Zip</button> data-i18n-key="extract_zip_button">Extract Zip</button>
<button id="createFileBtn" class="btn action-btn" data-i18n-key="create_file"> <div id="createDropdown" class="dropdown-container" style="position:relative; display:inline-block;">
${t('create_file')} <button id="createBtn" class="btn action-btn" data-i18n-key="create">
</button> ${t('create')} <span class="material-icons" style="font-size:16px;vertical-align:middle;">arrow_drop_down</span>
</button>
<ul
id="createMenu"
class="dropdown-menu"
style="
display: none;
position: absolute;
top: 100%;
left: 0;
margin: 4px 0 0;
padding: 0;
list-style: none;
background: #fff;
border: 1px solid #ccc;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
z-index: 1000;
min-width: 140px;
"
>
<li id="createFileOption" class="dropdown-item" data-i18n-key="create_file" style="padding:8px 12px; cursor:pointer;">
${t('create_file')}
</li>
<li id="createFolderOption" class="dropdown-item" data-i18n-key="create_folder" style="padding:8px 12px; cursor:pointer;">
${t('create_folder')}
</li>
</ul>
</div>
<!-- Create File Modal --> <!-- Create File Modal -->
<div id="createFileModal" class="modal" style="display:none;"> <div id="createFileModal" class="modal" style="display:none;">
<div class="modal-content"> <div class="modal-content">

View File

@@ -3,7 +3,7 @@ import { loadAdminConfigFunc } from './auth.js';
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
import { sendRequest } from './networkUtils.js'; import { sendRequest } from './networkUtils.js';
const version = "v1.3.5"; const version = "v1.3.8";
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`; const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
// ————— Inject updated styles ————— // ————— Inject updated styles —————

View File

@@ -33,54 +33,66 @@ export function toggleAllCheckboxes(masterCheckbox) {
export function updateFileActionButtons() { export function updateFileActionButtons() {
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox"); const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked"); const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
const deleteBtn = document.getElementById("deleteSelectedBtn");
const copyBtn = document.getElementById("copySelectedBtn"); const copyBtn = document.getElementById("copySelectedBtn");
const moveBtn = document.getElementById("moveSelectedBtn"); const moveBtn = document.getElementById("moveSelectedBtn");
const deleteBtn = document.getElementById("deleteSelectedBtn");
const zipBtn = document.getElementById("downloadZipBtn"); const zipBtn = document.getElementById("downloadZipBtn");
const extractZipBtn = document.getElementById("extractZipBtn"); const extractZipBtn = document.getElementById("extractZipBtn");
const createBtn = document.getElementById("createBtn");
// keep the “select all” in sync —— const anyFiles = fileCheckboxes.length > 0;
const master = document.getElementById("selectAll"); const anySelected = selectedCheckboxes.length > 0;
if (master) { const anyZip = Array.from(selectedCheckboxes)
if (selectedCheckboxes.length === fileCheckboxes.length) { .some(cb => cb.value.toLowerCase().endsWith(".zip"));
master.checked = true;
master.indeterminate = false;
} else if (selectedCheckboxes.length === 0) {
master.checked = false;
master.indeterminate = false;
} else {
master.checked = false;
master.indeterminate = true;
}
}
if (fileCheckboxes.length === 0) { // — Select All checkbox sync (unchanged) —
if (copyBtn) copyBtn.style.display = "none"; const master = document.getElementById("selectAll");
if (moveBtn) moveBtn.style.display = "none"; if (master) {
if (deleteBtn) deleteBtn.style.display = "none"; if (selectedCheckboxes.length === fileCheckboxes.length) {
if (zipBtn) zipBtn.style.display = "none"; master.checked = true;
if (extractZipBtn) extractZipBtn.style.display = "none"; master.indeterminate = false;
} else { } else if (selectedCheckboxes.length === 0) {
if (copyBtn) copyBtn.style.display = "inline-block"; master.checked = false;
if (moveBtn) moveBtn.style.display = "inline-block"; master.indeterminate = false;
if (deleteBtn) deleteBtn.style.display = "inline-block"; } else {
if (zipBtn) zipBtn.style.display = "inline-block"; master.checked = false;
if (extractZipBtn) extractZipBtn.style.display = "inline-block"; master.indeterminate = true;
const anySelected = selectedCheckboxes.length > 0;
if (copyBtn) copyBtn.disabled = !anySelected;
if (moveBtn) moveBtn.disabled = !anySelected;
if (deleteBtn) deleteBtn.disabled = !anySelected;
if (zipBtn) zipBtn.disabled = !anySelected;
if (extractZipBtn) {
// Enable only if at least one selected file ends with .zip (case-insensitive).
const anyZipSelected = Array.from(selectedCheckboxes).some(chk =>
chk.value.toLowerCase().endsWith(".zip")
);
extractZipBtn.disabled = !anyZipSelected;
} }
} }
// Delete / Copy / Move: only show when something is selected
if (deleteBtn) {
deleteBtn.style.display = anySelected ? "" : "none";
}
if (copyBtn) {
copyBtn.style.display = anySelected ? "" : "none";
}
if (moveBtn) {
moveBtn.style.display = anySelected ? "" : "none";
}
// Download ZIP: only show when something is selected
if (zipBtn) {
zipBtn.style.display = anySelected ? "" : "none";
}
// Extract ZIP: only show when a selected file is a .zip
if (extractZipBtn) {
extractZipBtn.style.display = anyZip ? "" : "none";
}
// Create File: only show when nothing is selected
if (createBtn) {
createBtn.style.display = anySelected ? "none" : "";
}
// Finally disable the ones that are shown but shouldnt be clickable
if (deleteBtn) deleteBtn.disabled = !anySelected;
if (copyBtn) copyBtn.disabled = !anySelected;
if (moveBtn) moveBtn.disabled = !anySelected;
if (zipBtn) zipBtn.disabled = !anySelected;
if (extractZipBtn) extractZipBtn.disabled = !anyZip;
} }
export function showToast(message, duration = 3000) { export function showToast(message, duration = 3000) {

View File

@@ -688,4 +688,35 @@ document.addEventListener("DOMContentLoaded", () => {
attachEnterKeyListener("downloadFileModal", "confirmSingleDownloadButton"); attachEnterKeyListener("downloadFileModal", "confirmSingleDownloadButton");
}); });
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('createBtn');
const menu = document.getElementById('createMenu');
const fileOpt = document.getElementById('createFileOption');
const folderOpt= document.getElementById('createFolderOption');
// Toggle dropdown on click
btn.addEventListener('click', (e) => {
e.stopPropagation();
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
});
// Create File
fileOpt.addEventListener('click', () => {
menu.style.display = 'none';
openCreateFileModal(); // your existing function
});
// Create Folder
folderOpt.addEventListener('click', () => {
menu.style.display = 'none';
document.getElementById('createFolderModal').style.display = 'block';
document.getElementById('newFolderName').focus();
});
// Close if you click anywhere else
document.addEventListener('click', () => {
menu.style.display = 'none';
});
});
window.renameFile = renameFile; window.renameFile = renameFile;

View File

@@ -16,7 +16,21 @@ import { t } from './i18n.js';
import { bindFileListContextMenu } from './fileMenu.js'; import { bindFileListContextMenu } from './fileMenu.js';
import { openDownloadModal } from './fileActions.js'; import { openDownloadModal } from './fileActions.js';
import { openTagModal, openMultiTagModal } from './fileTags.js'; import { openTagModal, openMultiTagModal } from './fileTags.js';
import { getParentFolder, updateBreadcrumbTitle, setupBreadcrumbDelegation } from './folderManager.js'; import {
getParentFolder,
updateBreadcrumbTitle,
setupBreadcrumbDelegation,
showFolderManagerContextMenu,
hideFolderManagerContextMenu,
openRenameFolderModal,
openDeleteFolderModal
} from './folderManager.js';
import { openFolderShareModal } from './folderShareModal.js';
import {
folderDragOverHandler,
folderDragLeaveHandler,
folderDropHandler
} from './fileDragDrop.js';
export let fileData = []; export let fileData = [];
export let sortOrder = { column: "uploaded", ascending: true }; export let sortOrder = { column: "uploaded", ascending: true };
@@ -190,7 +204,7 @@ window.updateRowHighlight = updateRowHighlight;
export async function loadFileList(folderParam) { export async function loadFileList(folderParam) {
const folder = folderParam || "root"; const folder = folderParam || "root";
const fileListContainer = document.getElementById("fileList"); const fileListContainer = document.getElementById("fileList");
const actionsContainer = document.getElementById("fileListActions"); const actionsContainer = document.getElementById("fileListActions");
// 1) show loader // 1) show loader
fileListContainer.style.visibility = "hidden"; fileListContainer.style.visibility = "hidden";
@@ -207,15 +221,15 @@ export async function loadFileList(folderParam) {
window.location.href = "/api/auth/logout.php"; window.location.href = "/api/auth/logout.php";
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
const data = await filesRes.json(); const data = await filesRes.json();
const folderRaw = await foldersRes.json(); const folderRaw = await foldersRes.json();
// --- build ONLY the *direct* children of current folder --- // --- build ONLY the *direct* children of current folder ---
let subfolders = []; let subfolders = [];
const hidden = new Set([ "profile_pics", "trash" ]); const hidden = new Set(["profile_pics", "trash"]);
if (Array.isArray(folderRaw)) { if (Array.isArray(folderRaw)) {
const allPaths = folderRaw.map(item => item.folder ?? item); const allPaths = folderRaw.map(item => item.folder ?? item);
const depth = folder === "root" ? 1 : folder.split("/").length + 1; const depth = folder === "root" ? 1 : folder.split("/").length + 1;
subfolders = allPaths subfolders = allPaths
.filter(p => { .filter(p => {
if (folder === "root") { if (folder === "root") {
@@ -235,17 +249,40 @@ export async function loadFileList(folderParam) {
if (!data.files || Object.keys(data.files).length === 0) { if (!data.files || Object.keys(data.files).length === 0) {
fileListContainer.textContent = t("no_files_found"); fileListContainer.textContent = t("no_files_found");
// hide summary // hide summary + slider
const summaryElem = document.getElementById("fileSummary"); const summaryElem = document.getElementById("fileSummary");
if (summaryElem) summaryElem.style.display = "none"; if (summaryElem) summaryElem.style.display = "none";
// hide slider
const sliderContainer = document.getElementById("viewSliderContainer"); const sliderContainer = document.getElementById("viewSliderContainer");
if (sliderContainer) sliderContainer.style.display = "none"; if (sliderContainer) sliderContainer.style.display = "none";
// hide folder strip // show/hide folder strip *even when there are no files*
const strip = document.getElementById("folderStripContainer"); let strip = document.getElementById("folderStripContainer");
if (strip) strip.style.display = "none"; if (!strip) {
strip = document.createElement("div");
strip.id = "folderStripContainer";
strip.className = "folder-strip-container";
actionsContainer.parentNode.insertBefore(strip, fileListContainer);
}
if (window.showFoldersInList && subfolders.length) {
strip.innerHTML = subfolders.map(sf => `
<div class="folder-item" data-folder="${sf.full}">
<i class="material-icons">folder</i>
<div class="folder-name">${escapeHTML(sf.name)}</div>
</div>
`).join("");
strip.style.display = "flex";
strip.querySelectorAll(".folder-item").forEach(el => {
el.addEventListener("click", () => {
const dest = el.dataset.folder;
window.currentFolder = dest;
localStorage.setItem("lastOpenedFolder", dest);
updateBreadcrumbTitle(dest);
loadFileList(dest);
});
});
} else {
strip.style.display = "none";
}
updateFileActionButtons(); updateFileActionButtons();
return []; return [];
@@ -261,7 +298,7 @@ export async function loadFileList(folderParam) {
data.files = data.files.map(f => { data.files = data.files.map(f => {
f.fullName = (f.path || f.name).trim().toLowerCase(); f.fullName = (f.path || f.name).trim().toLowerCase();
f.editable = canEditFile(f.name); f.editable = canEditFile(f.name);
f.folder = folder; f.folder = folder;
return f; return f;
}); });
fileData = data.files; fileData = data.files;
@@ -294,13 +331,13 @@ export async function loadFileList(folderParam) {
if (viewMode === "gallery") { if (viewMode === "gallery") {
const w = window.innerWidth; const w = window.innerWidth;
let maxCols; let maxCols;
if (w < 600) maxCols = 1; if (w < 600) maxCols = 1;
else if (w < 900) maxCols = 2; else if (w < 900) maxCols = 2;
else if (w < 1200) maxCols = 4; else if (w < 1200) maxCols = 4;
else maxCols = 6; else maxCols = 6;
const currentCols = Math.min( const currentCols = Math.min(
parseInt(localStorage.getItem("galleryColumns")||"3",10), parseInt(localStorage.getItem("galleryColumns") || "3", 10),
maxCols maxCols
); );
@@ -319,7 +356,7 @@ export async function loadFileList(folderParam) {
<span id="galleryColumnsValue" style="margin-left:6px;line-height:1;">${currentCols}</span> <span id="galleryColumnsValue" style="margin-left:6px;line-height:1;">${currentCols}</span>
`; `;
const gallerySlider = document.getElementById("galleryColumnsSlider"); const gallerySlider = document.getElementById("galleryColumnsSlider");
const galleryValue = document.getElementById("galleryColumnsValue"); const galleryValue = document.getElementById("galleryColumnsValue");
gallerySlider.oninput = e => { gallerySlider.oninput = e => {
const v = +e.target.value; const v = +e.target.value;
localStorage.setItem("galleryColumns", v); localStorage.setItem("galleryColumns", v);
@@ -328,16 +365,16 @@ export async function loadFileList(folderParam) {
?.style.setProperty("grid-template-columns", `repeat(${v},1fr)`); ?.style.setProperty("grid-template-columns", `repeat(${v},1fr)`);
}; };
} else { } else {
const currentHeight = parseInt(localStorage.getItem("rowHeight")||"48",10); const currentHeight = parseInt(localStorage.getItem("rowHeight") || "48", 10);
sliderContainer.innerHTML = ` sliderContainer.innerHTML = `
<label for="rowHeightSlider" style="margin-right:8px;line-height:1;"> <label for="rowHeightSlider" style="margin-right:8px;line-height:1;">
${t("row_height")}: ${t("row_height")}:
</label> </label>
<input type="range" id="rowHeightSlider" min="31" max="60" value="${currentHeight}" style="vertical-align:middle;"> <input type="range" id="rowHeightSlider" min="30" max="60" value="${currentHeight}" style="vertical-align:middle;">
<span id="rowHeightValue" style="margin-left:6px;line-height:1;">${currentHeight}px</span> <span id="rowHeightValue" style="margin-left:6px;line-height:1;">${currentHeight}px</span>
`; `;
const rowSlider = document.getElementById("rowHeightSlider"); const rowSlider = document.getElementById("rowHeightSlider");
const rowValue = document.getElementById("rowHeightValue"); const rowValue = document.getElementById("rowHeightValue");
rowSlider.oninput = e => { rowSlider.oninput = e => {
const v = e.target.value; const v = e.target.value;
document.documentElement.style.setProperty("--file-row-height", v + "px"); document.documentElement.style.setProperty("--file-row-height", v + "px");
@@ -355,29 +392,73 @@ export async function loadFileList(folderParam) {
strip.className = "folder-strip-container"; strip.className = "folder-strip-container";
actionsContainer.parentNode.insertBefore(strip, actionsContainer); actionsContainer.parentNode.insertBefore(strip, actionsContainer);
} }
if (window.showFoldersInList && subfolders.length) { if (window.showFoldersInList && subfolders.length) {
strip.innerHTML = subfolders.map(sf => ` strip.innerHTML = subfolders.map(sf => `
<div class="folder-item" data-folder="${sf.full}"> <div class="folder-item" data-folder="${sf.full}" draggable="true">
<i class="material-icons">folder</i> <i class="material-icons">folder</i>
<div class="folder-name">${escapeHTML(sf.name)}</div> <div class="folder-name">${escapeHTML(sf.name)}</div>
</div> </div>
`).join(""); `).join("");
strip.style.display = "flex"; strip.style.display = "flex";
// wire up each foldertile
strip.querySelectorAll(".folder-item").forEach(el => { strip.querySelectorAll(".folder-item").forEach(el => {
// 1) click to navigate
el.addEventListener("click", () => { el.addEventListener("click", () => {
const dest = el.dataset.folder; const dest = el.dataset.folder;
window.currentFolder = dest; window.currentFolder = dest;
localStorage.setItem("lastOpenedFolder", dest); localStorage.setItem("lastOpenedFolder", dest);
// sync breadcrumb & tree
updateBreadcrumbTitle(dest); updateBreadcrumbTitle(dest);
document.querySelectorAll(".folder-option.selected") document.querySelectorAll(".folder-option.selected").forEach(o => o.classList.remove("selected"));
.forEach(o => o.classList.remove("selected")); document.querySelector(`.folder-option[data-folder="${dest}"]`)?.classList.add("selected");
document.querySelector(`.folder-option[data-folder="${dest}"]`)
?.classList.add("selected");
// reload
loadFileList(dest); loadFileList(dest);
}); });
// 2) drag & drop
el.addEventListener("dragover", folderDragOverHandler);
el.addEventListener("dragleave", folderDragLeaveHandler);
el.addEventListener("drop", folderDropHandler);
// 3) right-click context menu
el.addEventListener("contextmenu", e => {
e.preventDefault();
e.stopPropagation();
const dest = el.dataset.folder;
window.currentFolder = dest;
localStorage.setItem("lastOpenedFolder", dest);
// highlight the strip tile
strip.querySelectorAll(".folder-item.selected").forEach(i => i.classList.remove("selected"));
el.classList.add("selected");
// reuse folderManager menu
const menuItems = [
{
label: t("create_folder"),
action: () => document.getElementById("createFolderModal").style.display = "block"
},
{
label: t("rename_folder"),
action: () => openRenameFolderModal()
},
{
label: t("folder_share"),
action: () => openFolderShareModal(dest)
},
{
label: t("delete_folder"),
action: () => openDeleteFolderModal()
}
];
showFolderManagerContextMenu(e.pageX, e.pageY, menuItems);
});
}); });
// one global click to hide any open context menu
document.addEventListener("click", hideFolderManagerContextMenu);
} else { } else {
strip.style.display = "none"; strip.style.display = "none";
} }

View File

@@ -551,7 +551,7 @@ export function loadFolderList(selectedFolder) {
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal); document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal); document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
function openRenameFolderModal() { export function openRenameFolderModal() {
const selectedFolder = window.currentFolder || "root"; const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") { if (!selectedFolder || selectedFolder === "root") {
showToast("Please select a valid folder to rename."); showToast("Please select a valid folder to rename.");
@@ -614,7 +614,7 @@ document.getElementById("submitRenameFolder").addEventListener("click", function
}); });
}); });
function openDeleteFolderModal() { export function openDeleteFolderModal() {
const selectedFolder = window.currentFolder || "root"; const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") { if (!selectedFolder || selectedFolder === "root") {
showToast("Please select a valid folder to delete."); showToast("Please select a valid folder to delete.");
@@ -718,7 +718,7 @@ document.getElementById("submitCreateFolder").addEventListener("click", async ()
}); });
// ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ---------- // ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ----------
function showFolderManagerContextMenu(x, y, menuItems) { export function showFolderManagerContextMenu(x, y, menuItems) {
let menu = document.getElementById("folderManagerContextMenu"); let menu = document.getElementById("folderManagerContextMenu");
if (!menu) { if (!menu) {
menu = document.createElement("div"); menu = document.createElement("div");
@@ -765,7 +765,7 @@ function showFolderManagerContextMenu(x, y, menuItems) {
menu.style.display = "block"; menu.style.display = "block";
} }
function hideFolderManagerContextMenu() { export function hideFolderManagerContextMenu() {
const menu = document.getElementById("folderManagerContextMenu"); const menu = document.getElementById("folderManagerContextMenu");
if (menu) { if (menu) {
menu.style.display = "none"; menu.style.display = "none";
@@ -796,7 +796,7 @@ function folderManagerContextMenuHandler(e) {
}, },
{ {
label: t("folder_share"), label: t("folder_share"),
action: () => { openFolderShareModal(); } action: () => { openFolderShareModal(folder); }
}, },
{ {
label: t("delete_folder"), label: t("delete_folder"),

View File

@@ -64,35 +64,26 @@ export function initializeApp() {
} }
export function loadCsrfToken() { export function loadCsrfToken() {
return fetchWithCsrf('/api/auth/token.php', { return fetchWithCsrf('/api/auth/token.php', { method: 'GET' })
method: 'GET'
})
.then(res => { .then(res => {
if (!res.ok) { if (!res.ok) throw new Error(`Token fetch failed with status ${res.status}`);
throw new Error(`Token fetch failed with status ${res.status}`);
}
return res.json(); return res.json();
}) })
.then(({ csrf_token, share_url }) => { .then(({ csrf_token, share_url }) => {
// Update global and <meta>
window.csrfToken = csrf_token; window.csrfToken = csrf_token;
let meta = document.querySelector('meta[name="csrf-token"]');
if (!meta) { // update CSRF meta
meta = document.createElement('meta'); let meta = document.querySelector('meta[name="csrf-token"]') ||
meta.name = 'csrf-token'; Object.assign(document.head.appendChild(document.createElement('meta')), { name: 'csrf-token' });
document.head.appendChild(meta);
}
meta.content = csrf_token; meta.content = csrf_token;
let shareMeta = document.querySelector('meta[name="share-url"]'); // force share_url to match wherever we're browsing
if (!shareMeta) { const actualShare = window.location.origin;
shareMeta = document.createElement('meta'); let shareMeta = document.querySelector('meta[name="share-url"]') ||
shareMeta.name = 'share-url'; Object.assign(document.head.appendChild(document.createElement('meta')), { name: 'share-url' });
document.head.appendChild(shareMeta); shareMeta.content = actualShare;
}
shareMeta.content = share_url;
return { csrf_token, share_url }; return { csrf_token, share_url: actualShare };
}); });
} }

View File

@@ -79,15 +79,16 @@ export function setupTrashRestoreDelete() {
body: JSON.stringify({ files }) body: JSON.stringify({ files })
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(() => {
if (data.success) { // Always report what we actually restored
showToast(data.success); if (files.length === 1) {
toggleVisibility("restoreFilesModal", false); showToast(`Restored file: ${files[0]}`);
loadFileList(window.currentFolder);
loadFolderTree(window.currentFolder);
} else { } else {
showToast(data.error); showToast(`Restored files: ${files.join(", ")}`);
} }
toggleVisibility("restoreFilesModal", false);
loadFileList(window.currentFolder);
loadFolderTree(window.currentFolder);
}) })
.catch(err => { .catch(err => {
console.error("Error restoring files:", err); console.error("Error restoring files:", err);
@@ -119,16 +120,15 @@ export function setupTrashRestoreDelete() {
body: JSON.stringify({ files }) body: JSON.stringify({ files })
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(() => {
if (data.success) { if (files.length === 1) {
showToast(data.success); showToast(`Restored file: ${files[0]}`);
toggleVisibility("restoreFilesModal", false);
loadFileList(window.currentFolder);
loadFolderTree(window.currentFolder);
} else { } else {
showToast(data.error); showToast(`Restored files: ${files.join(", ")}`);
} }
toggleVisibility("restoreFilesModal", false);
loadFileList(window.currentFolder);
loadFolderTree(window.currentFolder);
}) })
.catch(err => { .catch(err => {
console.error("Error restoring files:", err); console.error("Error restoring files:", err);