Add folder strip and “Create File” functionality (closes #36)
This commit is contained in:
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,5 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 5/19/2025
|
||||
|
||||
### Added Folder strip & Create File
|
||||
|
||||
- **Folder strip in file list**
|
||||
- `loadFileList` now fetches sub-folders in parallel from `/api/folder/getFolderList.php`.
|
||||
- Filters to only *direct* children of the current folder, hiding `profile_pics` and `trash`.
|
||||
- Injects a new `.folder-strip-container` just below the Files In above (summary + slider).
|
||||
- Clicking a folder in the strip updates:
|
||||
- the breadcrumb (via `updateBreadcrumbTitle`)
|
||||
- the tree selection highlight
|
||||
- reloads `loadFileList` for the chosen folder.
|
||||
|
||||
- **Create File feature**
|
||||
- New “Create New File” button added to the file-actions toolbar and context menu.
|
||||
- New endpoint `public/api/file/createFile.php` (handled by `FileController`/`FileModel`):
|
||||
- Creates an empty file if it doesn’t already exist.
|
||||
- Appends an entry to `<folder>_metadata.json` with `uploaded` timestamp and `uploader`.
|
||||
- `fileActions.js`:
|
||||
- Implemented `handleCreateFile()` to show a modal, POST to the new endpoint, and refresh the list.
|
||||
- Added translations for `create_new_file` and `newfile_placeholder`.
|
||||
|
||||
---
|
||||
|
||||
## Changees 5/15/2025
|
||||
|
||||
### Drag‐and‐Drop Upload extended to File List
|
||||
|
||||
15
public/api/file/createFile.php
Normal file
15
public/api/file/createFile.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
// public/api/file/createFile.php
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success'=>false,'error'=>'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$fc = new FileController();
|
||||
$fc->createFile();
|
||||
@@ -848,6 +848,11 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
|
||||
background-color: #00796B;
|
||||
}
|
||||
|
||||
#createFileBtn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#fileList button.edit-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
@@ -2243,3 +2248,32 @@ body.dark-mode .user-dropdown .user-menu .item:hover {
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.folder-strip-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.folder-strip-container .folder-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
width: 80px;
|
||||
color: inherit; /* icon will pick up text color */
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.folder-strip-container .folder-item i.material-icons {
|
||||
font-size: 28px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.folder-strip-container .folder-item i.material-icons {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.folder-strip-container .folder-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -11,13 +11,18 @@
|
||||
<meta name="share-url" content="">
|
||||
<style>
|
||||
/* hide the app shell until JS says otherwise */
|
||||
.main-wrapper { display: none; }
|
||||
.main-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* full-screen white overlay while we check auth */
|
||||
#loadingOverlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: var(--bg-color,#fff);
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--bg-color, #fff);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -386,6 +391,26 @@
|
||||
data-i18n-key="download_zip">Download ZIP</button>
|
||||
<button id="extractZipBtn" class="btn btn-sm btn-info" data-i18n-title="extract_zip"
|
||||
data-i18n-key="extract_zip_button">Extract Zip</button>
|
||||
<button id="createFileBtn" class="btn action-btn" data-i18n-key="create_file">
|
||||
${t('create_file')}
|
||||
</button>
|
||||
<!-- Create File Modal -->
|
||||
<div id="createFileModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h4 data-i18n-key="create_new_file">Create New File</h4>
|
||||
<input
|
||||
type="text"
|
||||
id="createFileNameInput"
|
||||
class="form-control"
|
||||
placeholder="Enter filename…"
|
||||
data-i18n-placeholder="newfile_placeholder"
|
||||
/>
|
||||
<div class="modal-footer" style="margin-top:1rem; text-align:right;">
|
||||
<button id="cancelCreateFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
||||
<button id="confirmCreateFile" class="btn btn-primary" data-i18n-key="create">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="downloadZipModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
|
||||
@@ -440,8 +465,7 @@
|
||||
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
||||
<div id="changePasswordModal" class="modal" style="display:none;">
|
||||
<div class="modal-content" style="max-width:400px; margin:auto;">
|
||||
<span id="closeChangePasswordModal"
|
||||
class="editor-close-btn">×</span>
|
||||
<span id="closeChangePasswordModal" class="editor-close-btn">×</span>
|
||||
<h3 data-i18n-key="change_password_title">Change Password</h3>
|
||||
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
||||
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { loadAdminConfigFunc } from './auth.js';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
|
||||
const version = "v1.3.4";
|
||||
const version = "v1.3.5";
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
||||
|
||||
// ————— Inject updated styles —————
|
||||
|
||||
@@ -196,7 +196,7 @@ export async function openUserPanel() {
|
||||
padding: 20px;
|
||||
max-width: 600px; width:90%;
|
||||
border-radius: 8px;
|
||||
overflow-y: auto; max-height: 415px;
|
||||
overflow-y: auto; max-height: 500px;
|
||||
border: ${isDark ? '1px solid #444' : '1px solid #ccc'};
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: none;
|
||||
@@ -301,11 +301,11 @@ export async function openUserPanel() {
|
||||
totpCb.id = 'userTOTPEnabled';
|
||||
totpCb.style.verticalAlign = 'middle';
|
||||
totpCb.checked = totp_enabled;
|
||||
totpCb.addEventListener('change', async function() {
|
||||
totpCb.addEventListener('change', async function () {
|
||||
const resp = await fetch('/api/updateUserPanel.php', {
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type':'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ totp_enabled: this.checked })
|
||||
@@ -328,14 +328,14 @@ export async function openUserPanel() {
|
||||
const langSel = document.createElement('select');
|
||||
langSel.id = 'languageSelector';
|
||||
langSel.className = 'form-select';
|
||||
['en','es','fr','de'].forEach(code => {
|
||||
['en', 'es', 'fr', 'de'].forEach(code => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = code;
|
||||
opt.textContent = t(code === 'en'? 'english' : code === 'es'? 'spanish' : code === 'fr'? 'french' : 'german');
|
||||
opt.textContent = t(code === 'en' ? 'english' : code === 'es' ? 'spanish' : code === 'fr' ? 'french' : 'german');
|
||||
langSel.appendChild(opt);
|
||||
});
|
||||
langSel.value = localStorage.getItem('language') || 'en';
|
||||
langSel.addEventListener('change', function() {
|
||||
langSel.addEventListener('change', function () {
|
||||
localStorage.setItem('language', this.value);
|
||||
setLocale(this.value);
|
||||
applyTranslations();
|
||||
@@ -343,8 +343,34 @@ export async function openUserPanel() {
|
||||
langFs.appendChild(langSel);
|
||||
content.appendChild(langFs);
|
||||
|
||||
// --- Display fieldset: “Show folders above files” ---
|
||||
const dispFs = document.createElement('fieldset');
|
||||
dispFs.style.marginBottom = '15px';
|
||||
const dispLegend = document.createElement('legend');
|
||||
dispLegend.textContent = t('display');
|
||||
dispFs.appendChild(dispLegend);
|
||||
const dispLabel = document.createElement('label');
|
||||
dispLabel.style.cursor = 'pointer';
|
||||
const dispCb = document.createElement('input');
|
||||
dispCb.type = 'checkbox';
|
||||
dispCb.id = 'showFoldersInList';
|
||||
dispCb.style.verticalAlign = 'middle';
|
||||
const stored = localStorage.getItem('showFoldersInList');
|
||||
dispCb.checked = stored === null ? true : stored === 'true';
|
||||
dispLabel.appendChild(dispCb);
|
||||
dispLabel.append(` ${t('show_folders_above_files')}`);
|
||||
dispFs.appendChild(dispLabel);
|
||||
content.appendChild(dispFs);
|
||||
|
||||
dispCb.addEventListener('change', () => {
|
||||
window.showFoldersInList = dispCb.checked;
|
||||
localStorage.setItem('showFoldersInList', dispCb.checked);
|
||||
// re‐load the entire file list (and strip) in one go:
|
||||
loadFileList(window.currentFolder);
|
||||
});
|
||||
|
||||
// wire up image‐input change
|
||||
fileInput.addEventListener('change', async function() {
|
||||
fileInput.addEventListener('change', async function () {
|
||||
const f = this.files[0];
|
||||
if (!f) return;
|
||||
// preview immediately
|
||||
|
||||
@@ -76,6 +76,72 @@ export function handleDownloadZipSelected(e) {
|
||||
}, 100);
|
||||
};
|
||||
|
||||
export function handleCreateFileSelected(e) {
|
||||
e.preventDefault(); e.stopImmediatePropagation();
|
||||
const modal = document.getElementById('createFileModal');
|
||||
modal.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
const inp = document.getElementById('newFileCreateName');
|
||||
if (inp) inp.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the “New File” modal
|
||||
*/
|
||||
export function openCreateFileModal() {
|
||||
const modal = document.getElementById('createFileModal');
|
||||
const input = document.getElementById('createFileNameInput');
|
||||
if (!modal || !input) {
|
||||
console.error('Create-file modal or input not found');
|
||||
return;
|
||||
}
|
||||
input.value = '';
|
||||
modal.style.display = 'block';
|
||||
setTimeout(() => input.focus(), 0);
|
||||
}
|
||||
|
||||
|
||||
export async function handleCreateFile(e) {
|
||||
e.preventDefault();
|
||||
const input = document.getElementById('createFileNameInput');
|
||||
if (!input) return console.error('Create-file input missing');
|
||||
const name = input.value.trim();
|
||||
if (!name) {
|
||||
showToast(t('newfile_placeholder')); // or a more explicit error
|
||||
return;
|
||||
}
|
||||
|
||||
const folder = window.currentFolder || 'root';
|
||||
try {
|
||||
const res = await fetch('/api/file/createFile.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type':'application/json',
|
||||
'X-CSRF-Token': window.csrfToken
|
||||
},
|
||||
// ⚠️ must send `name`, not `filename`
|
||||
body: JSON.stringify({ folder, name })
|
||||
});
|
||||
const js = await res.json();
|
||||
if (!js.success) throw new Error(js.error);
|
||||
showToast(t('file_created'));
|
||||
loadFileList(folder);
|
||||
} catch (err) {
|
||||
showToast(err.message || t('error_creating_file'));
|
||||
} finally {
|
||||
document.getElementById('createFileModal').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const cancel = document.getElementById('cancelCreateFile');
|
||||
const confirm = document.getElementById('confirmCreateFile');
|
||||
if (cancel) cancel.addEventListener('click', () => document.getElementById('createFileModal').style.display = 'none');
|
||||
if (confirm) confirm.addEventListener('click', handleCreateFile);
|
||||
});
|
||||
|
||||
export function openDownloadModal(fileName, folder) {
|
||||
// Store file details globally for the download confirmation function.
|
||||
window.singleFileToDownload = fileName;
|
||||
@@ -197,6 +263,49 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const progressModal = document.getElementById("downloadProgressModal");
|
||||
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
||||
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
||||
const cancelCreate = document.getElementById('cancelCreateFile');
|
||||
|
||||
if (cancelCreate) {
|
||||
cancelCreate.addEventListener('click', () => {
|
||||
document.getElementById('createFileModal').style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
const confirmCreate = document.getElementById('confirmCreateFile');
|
||||
if (confirmCreate) {
|
||||
confirmCreate.addEventListener('click', async () => {
|
||||
const name = document.getElementById('newFileCreateName').value.trim();
|
||||
if (!name) {
|
||||
showToast(t('please_enter_filename'));
|
||||
return;
|
||||
}
|
||||
document.getElementById('createFileModal').style.display = 'none';
|
||||
try {
|
||||
const res = await fetch('/api/file/createFile.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: window.currentFolder || 'root',
|
||||
filename: name
|
||||
})
|
||||
});
|
||||
const js = await res.json();
|
||||
if (!res.ok || !js.success) {
|
||||
throw new Error(js.error || t('error_creating_file'));
|
||||
}
|
||||
showToast(t('file_created_successfully'));
|
||||
loadFileList(window.currentFolder);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast(err.message || t('error_creating_file'));
|
||||
}
|
||||
});
|
||||
attachEnterKeyListener('createFileModal','confirmCreateFile');
|
||||
}
|
||||
|
||||
// 1) Cancel button hides the name modal
|
||||
if (cancelZipBtn) {
|
||||
@@ -553,8 +662,14 @@ export function initFileActions() {
|
||||
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
|
||||
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
|
||||
}
|
||||
const createBtn = document.getElementById('createFileBtn');
|
||||
if (createBtn) {
|
||||
createBtn.replaceWith(createBtn.cloneNode(true));
|
||||
document.getElementById('createFileBtn').addEventListener('click', openCreateFileModal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Hook up the single‐file download modal buttons
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const cancelDownloadFileBtn = document.getElementById("cancelDownloadFile");
|
||||
|
||||
@@ -16,6 +16,7 @@ import { t } from './i18n.js';
|
||||
import { bindFileListContextMenu } from './fileMenu.js';
|
||||
import { openDownloadModal } from './fileActions.js';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||||
import { getParentFolder, updateBreadcrumbTitle, setupBreadcrumbDelegation } from './folderManager.js';
|
||||
|
||||
export let fileData = [];
|
||||
export let sortOrder = { column: "uploaded", ascending: true };
|
||||
@@ -186,28 +187,51 @@ export function formatFolderName(folder) {
|
||||
window.toggleRowSelection = toggleRowSelection;
|
||||
window.updateRowHighlight = updateRowHighlight;
|
||||
|
||||
export function loadFileList(folderParam) {
|
||||
export async function loadFileList(folderParam) {
|
||||
const folder = folderParam || "root";
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
const actionsContainer = document.getElementById("fileListActions");
|
||||
|
||||
// 1) show loader
|
||||
fileListContainer.style.visibility = "hidden";
|
||||
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
|
||||
|
||||
return fetch(
|
||||
"/api/file/getFileList.php?folder=" +
|
||||
encodeURIComponent(folder) +
|
||||
"&recursive=1&t=" +
|
||||
Date.now()
|
||||
)
|
||||
.then((res) =>
|
||||
res.status === 401
|
||||
? (window.location.href = "/api/auth/logout.php" && Promise.reject("Unauthorized"))
|
||||
: res.json()
|
||||
)
|
||||
.then((data) => {
|
||||
try {
|
||||
// 2) fetch files + folders in parallel
|
||||
const [filesRes, foldersRes] = await Promise.all([
|
||||
fetch(`/api/file/getFileList.php?folder=${encodeURIComponent(folder)}&recursive=1&t=${Date.now()}`),
|
||||
fetch(`/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}`)
|
||||
]);
|
||||
|
||||
if (filesRes.status === 401) {
|
||||
window.location.href = "/api/auth/logout.php";
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
const data = await filesRes.json();
|
||||
const folderRaw = await foldersRes.json();
|
||||
|
||||
// --- build ONLY the *direct* children of current folder ---
|
||||
let subfolders = [];
|
||||
const hidden = new Set([ "profile_pics", "trash" ]);
|
||||
if (Array.isArray(folderRaw)) {
|
||||
const allPaths = folderRaw.map(item => item.folder ?? item);
|
||||
const depth = folder === "root" ? 1 : folder.split("/").length + 1;
|
||||
subfolders = allPaths
|
||||
.filter(p => {
|
||||
if (folder === "root") {
|
||||
return p.indexOf("/") === -1;
|
||||
}
|
||||
if (!p.startsWith(folder + "/")) return false;
|
||||
return p.split("/").length === depth;
|
||||
})
|
||||
.map(p => ({ name: p.split("/").pop(), full: p }));
|
||||
}
|
||||
subfolders = subfolders.filter(sf => !hidden.has(sf.name));
|
||||
|
||||
// 3) clear loader
|
||||
fileListContainer.innerHTML = "";
|
||||
|
||||
// No files case
|
||||
// 4) handle “no files” case
|
||||
if (!data.files || Object.keys(data.files).length === 0) {
|
||||
fileListContainer.textContent = t("no_files_found");
|
||||
|
||||
@@ -219,19 +243,22 @@ export function loadFileList(folderParam) {
|
||||
const sliderContainer = document.getElementById("viewSliderContainer");
|
||||
if (sliderContainer) sliderContainer.style.display = "none";
|
||||
|
||||
// hide folder strip
|
||||
const strip = document.getElementById("folderStripContainer");
|
||||
if (strip) strip.style.display = "none";
|
||||
|
||||
updateFileActionButtons();
|
||||
return [];
|
||||
}
|
||||
|
||||
// Normalize to array
|
||||
// 5) normalize files array
|
||||
if (!Array.isArray(data.files)) {
|
||||
data.files = Object.entries(data.files).map(([name, meta]) => {
|
||||
meta.name = name;
|
||||
return meta;
|
||||
});
|
||||
}
|
||||
// Enrich each file
|
||||
data.files = data.files.map((f) => {
|
||||
data.files = data.files.map(f => {
|
||||
f.fullName = (f.path || f.name).trim().toLowerCase();
|
||||
f.editable = canEditFile(f.name);
|
||||
f.folder = folder;
|
||||
@@ -239,10 +266,9 @@ export function loadFileList(folderParam) {
|
||||
});
|
||||
fileData = data.files;
|
||||
|
||||
// --- folder summary + slider injection ---
|
||||
const actionsContainer = document.getElementById("fileListActions");
|
||||
// 6) inject summary + slider
|
||||
if (actionsContainer) {
|
||||
// 1) summary
|
||||
// a) summary
|
||||
let summaryElem = document.getElementById("fileSummary");
|
||||
if (!summaryElem) {
|
||||
summaryElem = document.createElement("div");
|
||||
@@ -253,20 +279,19 @@ export function loadFileList(folderParam) {
|
||||
summaryElem.style.display = "block";
|
||||
summaryElem.innerHTML = buildFolderSummary(fileData);
|
||||
|
||||
// 2) view‐mode slider
|
||||
// b) slider
|
||||
const viewMode = window.viewMode || "table";
|
||||
let sliderContainer = document.getElementById("viewSliderContainer");
|
||||
if (!sliderContainer) {
|
||||
sliderContainer = document.createElement("div");
|
||||
sliderContainer.id = "viewSliderContainer";
|
||||
sliderContainer.style.cssText = "display: inline-flex; align-items: center; vertical-align: middle; margin-right: auto; font-size: 0.9em;";
|
||||
sliderContainer.style.cssText = "display:inline-flex; align-items:center; margin-right:auto; font-size:0.9em;";
|
||||
actionsContainer.insertBefore(sliderContainer, summaryElem);
|
||||
} else {
|
||||
sliderContainer.style.display = "inline-flex";
|
||||
}
|
||||
|
||||
if (viewMode === "gallery") {
|
||||
// determine responsive caps:
|
||||
const w = window.innerWidth;
|
||||
let maxCols;
|
||||
if (w < 600) maxCols = 1;
|
||||
@@ -275,12 +300,12 @@ export function loadFileList(folderParam) {
|
||||
else maxCols = 6;
|
||||
|
||||
const currentCols = Math.min(
|
||||
parseInt(localStorage.getItem("galleryColumns") || "3", 10),
|
||||
parseInt(localStorage.getItem("galleryColumns")||"3",10),
|
||||
maxCols
|
||||
);
|
||||
|
||||
sliderContainer.innerHTML = `
|
||||
<label for="galleryColumnsSlider" style="margin-right:8px; white-space:nowrap; line-height:1;">
|
||||
<label for="galleryColumnsSlider" style="margin-right:8px;line-height:1;">
|
||||
${t("columns")}:
|
||||
</label>
|
||||
<input
|
||||
@@ -291,32 +316,29 @@ export function loadFileList(folderParam) {
|
||||
value="${currentCols}"
|
||||
style="vertical-align:middle;"
|
||||
>
|
||||
<span id="galleryColumnsValue" style="margin-left:6px; line-height:1;">${currentCols}</span>
|
||||
<span id="galleryColumnsValue" style="margin-left:6px;line-height:1;">${currentCols}</span>
|
||||
`;
|
||||
// hookup gallery slider
|
||||
const gallerySlider = document.getElementById("galleryColumnsSlider");
|
||||
const galleryValue = document.getElementById("galleryColumnsValue");
|
||||
gallerySlider.oninput = (e) => {
|
||||
gallerySlider.oninput = e => {
|
||||
const v = +e.target.value;
|
||||
localStorage.setItem("galleryColumns", v);
|
||||
galleryValue.textContent = v;
|
||||
// update grid if already rendered
|
||||
const grid = document.querySelector(".gallery-container");
|
||||
if (grid) grid.style.gridTemplateColumns = `repeat(${v},1fr)`;
|
||||
document.querySelector(".gallery-container")
|
||||
?.style.setProperty("grid-template-columns", `repeat(${v},1fr)`);
|
||||
};
|
||||
} else {
|
||||
const currentHeight = parseInt(localStorage.getItem("rowHeight") ?? "48", 10);
|
||||
const currentHeight = parseInt(localStorage.getItem("rowHeight")||"48",10);
|
||||
sliderContainer.innerHTML = `
|
||||
<label for="rowHeightSlider" style="margin-right:8px; white-space:nowrap; line-height:1;">
|
||||
<label for="rowHeightSlider" style="margin-right:8px;line-height:1;">
|
||||
${t("row_height")}:
|
||||
</label>
|
||||
<input type="range" id="rowHeightSlider" min="31" 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>
|
||||
`;
|
||||
// hookup row‐height slider
|
||||
const rowSlider = document.getElementById("rowHeightSlider");
|
||||
const rowValue = document.getElementById("rowHeightValue");
|
||||
rowSlider.oninput = (e) => {
|
||||
rowSlider.oninput = e => {
|
||||
const v = e.target.value;
|
||||
document.documentElement.style.setProperty("--file-row-height", v + "px");
|
||||
localStorage.setItem("rowHeight", v);
|
||||
@@ -325,7 +347,42 @@ export function loadFileList(folderParam) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Render based on viewMode
|
||||
// 7) inject folder strip below actions, above file list
|
||||
let strip = document.getElementById("folderStripContainer");
|
||||
if (!strip) {
|
||||
strip = document.createElement("div");
|
||||
strip.id = "folderStripContainer";
|
||||
strip.className = "folder-strip-container";
|
||||
actionsContainer.parentNode.insertBefore(strip, actionsContainer);
|
||||
}
|
||||
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);
|
||||
// sync breadcrumb & tree
|
||||
updateBreadcrumbTitle(dest);
|
||||
document.querySelectorAll(".folder-option.selected")
|
||||
.forEach(o => o.classList.remove("selected"));
|
||||
document.querySelector(`.folder-option[data-folder="${dest}"]`)
|
||||
?.classList.add("selected");
|
||||
// reload
|
||||
loadFileList(dest);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
strip.style.display = "none";
|
||||
}
|
||||
|
||||
// 8) render files
|
||||
if (window.viewMode === "gallery") {
|
||||
renderGalleryView(folder);
|
||||
} else {
|
||||
@@ -334,23 +391,22 @@ export function loadFileList(folderParam) {
|
||||
|
||||
updateFileActionButtons();
|
||||
return data.files;
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error loading file list:", err);
|
||||
if (err !== "Unauthorized") {
|
||||
if (err.message !== "Unauthorized") {
|
||||
fileListContainer.textContent = "Error loading files.";
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.finally(() => {
|
||||
} finally {
|
||||
fileListContainer.style.visibility = "visible";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update renderFileTable so it writes its content into the provided container.
|
||||
*/
|
||||
export function renderFileTable(folder, container) {
|
||||
export function renderFileTable(folder, container, subfolders) {
|
||||
const fileListContent = container || document.getElementById("fileList");
|
||||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||||
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
||||
@@ -408,6 +464,10 @@ export function renderFileTable(folder, container) {
|
||||
|
||||
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||
|
||||
fileListContent.querySelectorAll('.folder-item').forEach(el => {
|
||||
el.addEventListener('click', () => loadFileList(el.dataset.folder));
|
||||
});
|
||||
|
||||
// pagination clicks
|
||||
const prevBtn = document.getElementById("prevPageBtn");
|
||||
if (prevBtn) prevBtn.addEventListener("click", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// fileMenu.js
|
||||
import { updateRowHighlight, showToast } from './domUtils.js';
|
||||
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile } from './fileActions.js';
|
||||
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile, openCreateFileModal } from './fileActions.js';
|
||||
import { previewFile } from './filePreview.js';
|
||||
import { editFile } from './fileEditor.js';
|
||||
import { canEditFile, fileData } from './fileListView.js';
|
||||
@@ -75,6 +75,7 @@ export function fileListContextMenuHandler(e) {
|
||||
const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value);
|
||||
|
||||
let menuItems = [
|
||||
{ label: t("create_file"), action: () => openCreateFileModal() },
|
||||
{ label: t("delete_selected"), action: () => { handleDeleteSelected(new Event("click")); } },
|
||||
{ label: t("copy_selected"), action: () => { handleCopySelected(new Event("click")); } },
|
||||
{ label: t("move_selected"), action: () => { handleMoveSelected(new Event("click")); } },
|
||||
|
||||
@@ -56,7 +56,7 @@ function saveFolderTreeState(state) {
|
||||
}
|
||||
|
||||
// Helper for getting the parent folder.
|
||||
function getParentFolder(folder) {
|
||||
export function getParentFolder(folder) {
|
||||
if (folder === "root") return "root";
|
||||
const lastSlash = folder.lastIndexOf("/");
|
||||
return lastSlash === -1 ? "root" : folder.substring(0, lastSlash);
|
||||
@@ -361,7 +361,7 @@ function renderBreadcrumbFragment(folderPath) {
|
||||
return frag;
|
||||
}
|
||||
|
||||
function updateBreadcrumbTitle(folder) {
|
||||
export function updateBreadcrumbTitle(folder) {
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
titleEl.textContent = "";
|
||||
titleEl.appendChild(document.createTextNode(t("files_in") + " ("));
|
||||
|
||||
@@ -266,7 +266,16 @@ const translations = {
|
||||
"items_per_page": "items per page",
|
||||
"columns": "Columns",
|
||||
"row_height": "Row Height",
|
||||
"api_docs": "API Docs"
|
||||
"api_docs": "API Docs",
|
||||
"show_folders_above_files": "Show folders above files",
|
||||
"display": "Display",
|
||||
"create_file": "Create File",
|
||||
"create_new_file": "Create New File",
|
||||
"enter_file_name": "Enter file name",
|
||||
"newfile_placeholder": "New file name",
|
||||
"file_created_successfully": "File created successfully!",
|
||||
"error_creating_file": "Error creating file",
|
||||
"file_created": "File created successfully!"
|
||||
},
|
||||
es: {
|
||||
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
||||
|
||||
@@ -20,7 +20,8 @@ export function initializeApp() {
|
||||
window.currentFolder = "root";
|
||||
initTagSearch();
|
||||
loadFileList(window.currentFolder);
|
||||
|
||||
const stored = localStorage.getItem('showFoldersInList');
|
||||
window.showFoldersInList = stored === null ? true : stored === 'true';
|
||||
const fileListArea = document.getElementById('fileListContainer');
|
||||
const uploadArea = document.getElementById('uploadDropArea');
|
||||
if (fileListArea && uploadArea) {
|
||||
|
||||
@@ -1626,4 +1626,31 @@ class FileController
|
||||
echo json_encode(['success' => false, 'error' => 'Not found']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/file/createFile.php
|
||||
*/
|
||||
public function createFile(): void
|
||||
{
|
||||
|
||||
// Check user permissions (assuming loadUserPermissions() is available).
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (!empty($userPermissions['readOnly'])) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create files."]);
|
||||
exit;
|
||||
}
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$folder = $body['folder'] ?? 'root';
|
||||
$filename = $body['name'] ?? '';
|
||||
|
||||
$result = FileModel::createFile($folder, $filename, $_SESSION['username'] ?? 'Unknown');
|
||||
|
||||
if (!$result['success']) {
|
||||
http_response_code($result['code'] ?? 400);
|
||||
echo json_encode(['success'=>false,'error'=>$result['error']]);
|
||||
} else {
|
||||
echo json_encode(['success'=>true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,16 +340,14 @@ class FolderController
|
||||
public function getFolderList(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Optionally, you might add further input validation if necessary.
|
||||
$folderList = FolderModel::getFolderList();
|
||||
$parent = $_GET['folder'] ?? null;
|
||||
$folderList = FolderModel::getFolderList($parent);
|
||||
echo json_encode($folderList);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -1278,4 +1278,64 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
||||
file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty file plus metadata entry.
|
||||
*
|
||||
* @param string $folder
|
||||
* @param string $filename
|
||||
* @param string $uploader
|
||||
* @return array ['success'=>bool, 'error'=>string, 'code'=>int]
|
||||
*/
|
||||
public static function createFile(string $folder, string $filename, string $uploader): array
|
||||
{
|
||||
// 1) basic validation
|
||||
if (!preg_match('/^[\w\-. ]+$/', $filename)) {
|
||||
return ['success'=>false,'error'=>'Invalid filename','code'=>400];
|
||||
}
|
||||
|
||||
// 2) build target path
|
||||
$base = UPLOAD_DIR;
|
||||
if ($folder !== 'root') {
|
||||
$base = rtrim(UPLOAD_DIR, '/\\')
|
||||
. DIRECTORY_SEPARATOR . $folder
|
||||
. DIRECTORY_SEPARATOR;
|
||||
}
|
||||
if (!is_dir($base) && !mkdir($base, 0775, true)) {
|
||||
return ['success'=>false,'error'=>'Cannot create folder','code'=>500];
|
||||
}
|
||||
$path = $base . $filename;
|
||||
|
||||
// 3) no overwrite
|
||||
if (file_exists($path)) {
|
||||
return ['success'=>false,'error'=>'File already exists','code'=>400];
|
||||
}
|
||||
|
||||
// 4) touch the file
|
||||
if (false === @file_put_contents($path, '')) {
|
||||
return ['success'=>false,'error'=>'Could not create file','code'=>500];
|
||||
}
|
||||
|
||||
// 5) write metadata
|
||||
$metaKey = ($folder === 'root') ? 'root' : $folder;
|
||||
$metaName = str_replace(['/', '\\', ' '], '-', $metaKey) . '_metadata.json';
|
||||
$metaPath = META_DIR . $metaName;
|
||||
|
||||
$collection = [];
|
||||
if (file_exists($metaPath)) {
|
||||
$json = file_get_contents($metaPath);
|
||||
$collection = json_decode($json, true) ?: [];
|
||||
}
|
||||
|
||||
$collection[$filename] = [
|
||||
'uploaded' => date(DATE_TIME_FORMAT),
|
||||
'uploader' => $uploader
|
||||
];
|
||||
|
||||
if (false === file_put_contents($metaPath, json_encode($collection, JSON_PRETTY_PRINT))) {
|
||||
return ['success'=>false,'error'=>'Failed to update metadata','code'=>500];
|
||||
}
|
||||
|
||||
return ['success'=>true];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user