Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f39b3a41e | ||
|
|
40cecc10ad | ||
|
|
aee78c9750 | ||
|
|
16ccb66d55 | ||
|
|
9209f7a582 | ||
|
|
4a736b0224 | ||
|
|
f162a7d0d7 | ||
|
|
3fc526df7f | ||
|
|
20422cf5a7 |
122
CHANGELOG.md
122
CHANGELOG.md
@@ -1,5 +1,127 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Changes 5/27/2025 v1.3.9
|
||||||
|
|
||||||
|
- Support for mounting CIFS (SMB) network shares via Docker volumes
|
||||||
|
- New `scripts/scan_uploads.php` script to generate metadata for imported files and folders
|
||||||
|
- `SCAN_ON_START` environment variable to trigger automatic scanning on container startup
|
||||||
|
- Documentation for configuring CIFS share mounting and scanning
|
||||||
|
|
||||||
|
- Clipboard Paste Upload Support (single image):
|
||||||
|
- Users can now paste images directly into the FileRise web interface.
|
||||||
|
- Pasted images are renamed to `image<TIMESTAMP>.png` and added to the upload queue using the existing drag-and-drop logic.
|
||||||
|
- Implemented using a `.isClipboard` flag and a delayed UI cleanup inside `xhr.addEventListener("load", ...)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 5/26/2025
|
||||||
|
|
||||||
|
- Updated `REGEX_FOLDER_NAME` in `config.php` to forbids < > : " | ? * characters in folder names.
|
||||||
|
- Ensures the whole name can’t end in a space or period.
|
||||||
|
- Blocks Windows device names.
|
||||||
|
|
||||||
|
- Updated `FolderController.php` when `createFolder` issues invalid folder name to return `http_response_code(400);`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 strip’s `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
|
||||||
|
|
||||||
|
- **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
|
||||||
|
|
||||||
|
- **Forward file‐list drops**
|
||||||
|
Dropping files onto the file‐list area (`#fileListContainer`) now re‐dispatches the same `drop` event to the upload card’s drop zone (`#uploadDropArea`)
|
||||||
|
- **Visual feedback**
|
||||||
|
Added a `.drop-hover` class on `#fileListContainer` during drag‐over for a dashed‐border + light‐background hover state to indicate it accepts file drops.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 5/14/2025 v1.3.4
|
## Changes 5/14/2025 v1.3.4
|
||||||
|
|
||||||
### 1. Button Grouping (Bootstrap)
|
### 1. Button Grouping (Bootstrap)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ define('TRASH_DIR', UPLOAD_DIR . 'trash/');
|
|||||||
define('TIMEZONE', 'America/New_York');
|
define('TIMEZONE', 'America/New_York');
|
||||||
define('DATE_TIME_FORMAT','m/d/y h:iA');
|
define('DATE_TIME_FORMAT','m/d/y h:iA');
|
||||||
define('TOTAL_UPLOAD_SIZE','5G');
|
define('TOTAL_UPLOAD_SIZE','5G');
|
||||||
define('REGEX_FOLDER_NAME', '/^[\p{L}\p{N}_\-\s\/\\\\]+$/u');
|
define('REGEX_FOLDER_NAME','/^(?!^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$)(?!.*[. ]$)(?:[^<>:"\/\\\\|?*\x00-\x1F]{1,255})(?:[\/\\\\][^<>:"\/\\\\|?*\x00-\x1F]{1,255})*$/xu');
|
||||||
define('PATTERN_FOLDER_NAME','[\p{L}\p{N}_\-\s\/\\\\]+');
|
define('PATTERN_FOLDER_NAME','[\p{L}\p{N}_\-\s\/\\\\]+');
|
||||||
define('REGEX_FILE_NAME', '/^[^\x00-\x1F\/\\\\]{1,255}$/u');
|
define('REGEX_FILE_NAME', '/^[^\x00-\x1F\/\\\\]{1,255}$/u');
|
||||||
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
||||||
|
|||||||
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,27 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
|
|||||||
background-color: #00796B;
|
background-color: #00796B;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#createBtn {
|
||||||
|
background-color: #007bff;
|
||||||
|
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;
|
||||||
@@ -966,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;
|
||||||
@@ -983,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
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
@@ -2242,4 +2269,40 @@ body.dark-mode .user-dropdown .user-menu .item:hover {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
white-space: nowrap;
|
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;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
.folder-strip-container .folder-item i.material-icons {
|
||||||
|
font-size: 28px;
|
||||||
|
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 {
|
||||||
|
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="">
|
<meta name="share-url" content="">
|
||||||
<style>
|
<style>
|
||||||
/* hide the app shell until JS says otherwise */
|
/* hide the app shell until JS says otherwise */
|
||||||
.main-wrapper { display: none; }
|
.main-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* full-screen white overlay while we check auth */
|
/* full-screen white overlay while we check auth */
|
||||||
#loadingOverlay {
|
#loadingOverlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0; left: 0; right: 0; bottom: 0;
|
top: 0;
|
||||||
background: var(--bg-color,#fff);
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--bg-color, #fff);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -384,8 +389,55 @@
|
|||||||
</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>
|
||||||
|
<div id="createDropdown" class="dropdown-container" style="position:relative; display:inline-block;">
|
||||||
|
<button id="createBtn" class="btn action-btn" data-i18n-key="create">
|
||||||
|
${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 -->
|
||||||
|
<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 id="downloadZipModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
|
<h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
|
||||||
@@ -440,8 +492,7 @@
|
|||||||
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
||||||
<div id="changePasswordModal" class="modal" style="display:none;">
|
<div id="changePasswordModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content" style="max-width:400px; margin:auto;">
|
<div class="modal-content" style="max-width:400px; margin:auto;">
|
||||||
<span id="closeChangePasswordModal"
|
<span id="closeChangePasswordModal" class="editor-close-btn">×</span>
|
||||||
class="editor-close-btn">×</span>
|
|
||||||
<h3 data-i18n-key="change_password_title">Change Password</h3>
|
<h3 data-i18n-key="change_password_title">Change Password</h3>
|
||||||
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
||||||
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
||||||
@@ -459,15 +510,15 @@
|
|||||||
<form id="addUserForm">
|
<form id="addUserForm">
|
||||||
<label for="newUsername" data-i18n-key="username">Username:</label>
|
<label for="newUsername" data-i18n-key="username">Username:</label>
|
||||||
<input type="text" id="newUsername" class="form-control" required />
|
<input type="text" id="newUsername" class="form-control" required />
|
||||||
|
|
||||||
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
||||||
<input type="password" id="addUserPassword" class="form-control" required />
|
<input type="password" id="addUserPassword" class="form-control" required />
|
||||||
|
|
||||||
<div id="adminCheckboxContainer">
|
<div id="adminCheckboxContainer">
|
||||||
<input type="checkbox" id="isAdmin" />
|
<input type="checkbox" id="isAdmin" />
|
||||||
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-container">
|
<div class="button-container">
|
||||||
<!-- Cancel stays type="button" -->
|
<!-- Cancel stays type="button" -->
|
||||||
<button type="button" id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">
|
<button type="button" id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">
|
||||||
|
|||||||
@@ -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.4";
|
const version = "v1.3.9";
|
||||||
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 —————
|
||||||
|
|||||||
@@ -184,11 +184,11 @@ function normalizePicUrl(raw) {
|
|||||||
export async function openUserPanel() {
|
export async function openUserPanel() {
|
||||||
// 1) load data
|
// 1) load data
|
||||||
const { username = 'User', profile_picture = '', totp_enabled = false } = await fetchCurrentUser();
|
const { username = 'User', profile_picture = '', totp_enabled = false } = await fetchCurrentUser();
|
||||||
const raw = profile_picture;
|
const raw = profile_picture;
|
||||||
const picUrl = normalizePicUrl(raw) || '/assets/default-avatar.png';
|
const picUrl = normalizePicUrl(raw) || '/assets/default-avatar.png';
|
||||||
|
|
||||||
// 2) dark‐mode helpers
|
// 2) dark‐mode helpers
|
||||||
const isDark = document.body.classList.contains('dark-mode');
|
const isDark = document.body.classList.contains('dark-mode');
|
||||||
const overlayBg = isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.3)';
|
const overlayBg = isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.3)';
|
||||||
const contentStyle = `
|
const contentStyle = `
|
||||||
background: ${isDark ? '#2c2c2c' : '#fff'};
|
background: ${isDark ? '#2c2c2c' : '#fff'};
|
||||||
@@ -196,7 +196,7 @@ export async function openUserPanel() {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
max-width: 600px; width:90%;
|
max-width: 600px; width:90%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow-y: auto; max-height: 415px;
|
overflow-y: auto; max-height: 500px;
|
||||||
border: ${isDark ? '1px solid #444' : '1px solid #ccc'};
|
border: ${isDark ? '1px solid #444' : '1px solid #ccc'};
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
@@ -210,16 +210,16 @@ export async function openUserPanel() {
|
|||||||
modal = document.createElement('div');
|
modal = document.createElement('div');
|
||||||
modal.id = 'userPanelModal';
|
modal.id = 'userPanelModal';
|
||||||
Object.assign(modal.style, {
|
Object.assign(modal.style, {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: '0',
|
top: '0',
|
||||||
left: '0',
|
left: '0',
|
||||||
right: '0',
|
right: '0',
|
||||||
bottom: '0',
|
bottom: '0',
|
||||||
background: overlayBg,
|
background: overlayBg,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
zIndex: '1000',
|
zIndex: '1000',
|
||||||
});
|
});
|
||||||
|
|
||||||
// content container
|
// content container
|
||||||
@@ -264,7 +264,7 @@ export async function openUserPanel() {
|
|||||||
avatarInner.appendChild(label);
|
avatarInner.appendChild(label);
|
||||||
const fileInput = document.createElement('input');
|
const fileInput = document.createElement('input');
|
||||||
fileInput.type = 'file';
|
fileInput.type = 'file';
|
||||||
fileInput.id = 'profilePicInput';
|
fileInput.id = 'profilePicInput';
|
||||||
fileInput.accept = 'image/*';
|
fileInput.accept = 'image/*';
|
||||||
fileInput.style.display = 'none';
|
fileInput.style.display = 'none';
|
||||||
avatarInner.appendChild(fileInput);
|
avatarInner.appendChild(fileInput);
|
||||||
@@ -301,11 +301,11 @@ export async function openUserPanel() {
|
|||||||
totpCb.id = 'userTOTPEnabled';
|
totpCb.id = 'userTOTPEnabled';
|
||||||
totpCb.style.verticalAlign = 'middle';
|
totpCb.style.verticalAlign = 'middle';
|
||||||
totpCb.checked = totp_enabled;
|
totpCb.checked = totp_enabled;
|
||||||
totpCb.addEventListener('change', async function() {
|
totpCb.addEventListener('change', async function () {
|
||||||
const resp = await fetch('/api/updateUserPanel.php', {
|
const resp = await fetch('/api/updateUserPanel.php', {
|
||||||
method: 'POST', credentials: 'include',
|
method: 'POST', credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type':'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRF-Token': window.csrfToken
|
'X-CSRF-Token': window.csrfToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ totp_enabled: this.checked })
|
body: JSON.stringify({ totp_enabled: this.checked })
|
||||||
@@ -328,14 +328,14 @@ export async function openUserPanel() {
|
|||||||
const langSel = document.createElement('select');
|
const langSel = document.createElement('select');
|
||||||
langSel.id = 'languageSelector';
|
langSel.id = 'languageSelector';
|
||||||
langSel.className = 'form-select';
|
langSel.className = 'form-select';
|
||||||
['en','es','fr','de'].forEach(code => {
|
['en', 'es', 'fr', 'de'].forEach(code => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = code;
|
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.appendChild(opt);
|
||||||
});
|
});
|
||||||
langSel.value = localStorage.getItem('language') || 'en';
|
langSel.value = localStorage.getItem('language') || 'en';
|
||||||
langSel.addEventListener('change', function() {
|
langSel.addEventListener('change', function () {
|
||||||
localStorage.setItem('language', this.value);
|
localStorage.setItem('language', this.value);
|
||||||
setLocale(this.value);
|
setLocale(this.value);
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
@@ -343,8 +343,34 @@ export async function openUserPanel() {
|
|||||||
langFs.appendChild(langSel);
|
langFs.appendChild(langSel);
|
||||||
content.appendChild(langFs);
|
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
|
// wire up image‐input change
|
||||||
fileInput.addEventListener('change', async function() {
|
fileInput.addEventListener('change', async function () {
|
||||||
const f = this.files[0];
|
const f = this.files[0];
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
// preview immediately
|
// preview immediately
|
||||||
@@ -357,13 +383,13 @@ export async function openUserPanel() {
|
|||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('profile_picture', f);
|
fd.append('profile_picture', f);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/profile/uploadPicture.php', {
|
const res = await fetch('/api/profile/uploadPicture.php', {
|
||||||
method: 'POST', credentials: 'include',
|
method: 'POST', credentials: 'include',
|
||||||
headers: { 'X-CSRF-Token': window.csrfToken },
|
headers: { 'X-CSRF-Token': window.csrfToken },
|
||||||
body: fd
|
body: fd
|
||||||
});
|
});
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
const js = JSON.parse(text || '{}');
|
const js = JSON.parse(text || '{}');
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
showToast(js.error || t('error_updating_picture'));
|
showToast(js.error || t('error_updating_picture'));
|
||||||
return;
|
return;
|
||||||
@@ -387,9 +413,9 @@ export async function openUserPanel() {
|
|||||||
Object.assign(modal.style, { background: overlayBg });
|
Object.assign(modal.style, { background: overlayBg });
|
||||||
const content = modal.querySelector('.modal-content');
|
const content = modal.querySelector('.modal-content');
|
||||||
content.style.cssText = contentStyle;
|
content.style.cssText = contentStyle;
|
||||||
modal.querySelector('#profilePicPreview').src = picUrl || '/assets/default-avatar.png';
|
modal.querySelector('#profilePicPreview').src = picUrl || '/assets/default-avatar.png';
|
||||||
modal.querySelector('#userTOTPEnabled').checked = totp_enabled;
|
modal.querySelector('#userTOTPEnabled').checked = totp_enabled;
|
||||||
modal.querySelector('#languageSelector').value = localStorage.getItem('language') || 'en';
|
modal.querySelector('#languageSelector').value = localStorage.getItem('language') || 'en';
|
||||||
modal.querySelector('h3').textContent = `${t('user_panel')} (${username})`;
|
modal.querySelector('h3').textContent = `${t('user_panel')} (${username})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +505,7 @@ export function openTOTPModal() {
|
|||||||
else showToast(t("error_generating_recovery_code") + ": " + (data.message || t("unknown_error")));
|
else showToast(t("error_generating_recovery_code") + ": " + (data.message || t("unknown_error")));
|
||||||
closeTOTPModal(false);
|
closeTOTPModal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Focus the input and attach enter key listener
|
// Focus the input and attach enter key listener
|
||||||
const totpConfirmInput = document.getElementById("totpConfirmInput");
|
const totpConfirmInput = document.getElementById("totpConfirmInput");
|
||||||
if (totpConfirmInput) {
|
if (totpConfirmInput) {
|
||||||
|
|||||||
@@ -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 shouldn’t 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) {
|
||||||
|
|||||||
@@ -76,6 +76,72 @@ export function handleDownloadZipSelected(e) {
|
|||||||
}, 100);
|
}, 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) {
|
export function openDownloadModal(fileName, folder) {
|
||||||
// Store file details globally for the download confirmation function.
|
// Store file details globally for the download confirmation function.
|
||||||
window.singleFileToDownload = fileName;
|
window.singleFileToDownload = fileName;
|
||||||
@@ -197,6 +263,49 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const progressModal = document.getElementById("downloadProgressModal");
|
const progressModal = document.getElementById("downloadProgressModal");
|
||||||
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
||||||
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
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
|
// 1) Cancel button hides the name modal
|
||||||
if (cancelZipBtn) {
|
if (cancelZipBtn) {
|
||||||
@@ -553,8 +662,14 @@ export function initFileActions() {
|
|||||||
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
|
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
|
||||||
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
|
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
|
// Hook up the single‐file download modal buttons
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const cancelDownloadFileBtn = document.getElementById("cancelDownloadFile");
|
const cancelDownloadFileBtn = document.getElementById("cancelDownloadFile");
|
||||||
@@ -573,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;
|
||||||
@@ -16,6 +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,
|
||||||
|
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 };
|
||||||
@@ -186,171 +201,293 @@ export function formatFolderName(folder) {
|
|||||||
window.toggleRowSelection = toggleRowSelection;
|
window.toggleRowSelection = toggleRowSelection;
|
||||||
window.updateRowHighlight = updateRowHighlight;
|
window.updateRowHighlight = updateRowHighlight;
|
||||||
|
|
||||||
export 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");
|
||||||
|
|
||||||
|
// 1) show loader
|
||||||
fileListContainer.style.visibility = "hidden";
|
fileListContainer.style.visibility = "hidden";
|
||||||
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
|
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
|
||||||
|
|
||||||
return fetch(
|
try {
|
||||||
"/api/file/getFileList.php?folder=" +
|
// 2) fetch files + folders in parallel
|
||||||
encodeURIComponent(folder) +
|
const [filesRes, foldersRes] = await Promise.all([
|
||||||
"&recursive=1&t=" +
|
fetch(`/api/file/getFileList.php?folder=${encodeURIComponent(folder)}&recursive=1&t=${Date.now()}`),
|
||||||
Date.now()
|
fetch(`/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}`)
|
||||||
)
|
]);
|
||||||
.then((res) =>
|
|
||||||
res.status === 401
|
|
||||||
? (window.location.href = "/api/auth/logout.php" && Promise.reject("Unauthorized"))
|
|
||||||
: res.json()
|
|
||||||
)
|
|
||||||
.then((data) => {
|
|
||||||
fileListContainer.innerHTML = "";
|
|
||||||
|
|
||||||
// No files case
|
if (filesRes.status === 401) {
|
||||||
if (!data.files || Object.keys(data.files).length === 0) {
|
window.location.href = "/api/auth/logout.php";
|
||||||
fileListContainer.textContent = t("no_files_found");
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
const data = await filesRes.json();
|
||||||
|
const folderRaw = await foldersRes.json();
|
||||||
|
|
||||||
// hide summary
|
// --- build ONLY the *direct* children of current folder ---
|
||||||
const summaryElem = document.getElementById("fileSummary");
|
let subfolders = [];
|
||||||
if (summaryElem) summaryElem.style.display = "none";
|
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));
|
||||||
|
|
||||||
// hide slider
|
// 3) clear loader
|
||||||
const sliderContainer = document.getElementById("viewSliderContainer");
|
fileListContainer.innerHTML = "";
|
||||||
if (sliderContainer) sliderContainer.style.display = "none";
|
|
||||||
|
|
||||||
updateFileActionButtons();
|
// 4) handle “no files” case
|
||||||
return [];
|
if (!data.files || Object.keys(data.files).length === 0) {
|
||||||
|
fileListContainer.textContent = t("no_files_found");
|
||||||
|
|
||||||
|
// hide summary + slider
|
||||||
|
const summaryElem = document.getElementById("fileSummary");
|
||||||
|
if (summaryElem) summaryElem.style.display = "none";
|
||||||
|
const sliderContainer = document.getElementById("viewSliderContainer");
|
||||||
|
if (sliderContainer) sliderContainer.style.display = "none";
|
||||||
|
|
||||||
|
// show/hide folder strip *even when there are no files*
|
||||||
|
let strip = document.getElementById("folderStripContainer");
|
||||||
|
if (!strip) {
|
||||||
|
strip = document.createElement("div");
|
||||||
|
strip.id = "folderStripContainer";
|
||||||
|
strip.className = "folder-strip-container";
|
||||||
|
actionsContainer.parentNode.insertBefore(strip, fileListContainer);
|
||||||
}
|
}
|
||||||
|
if (window.showFoldersInList && subfolders.length) {
|
||||||
// Normalize to array
|
strip.innerHTML = subfolders.map(sf => `
|
||||||
if (!Array.isArray(data.files)) {
|
<div class="folder-item" data-folder="${sf.full}">
|
||||||
data.files = Object.entries(data.files).map(([name, meta]) => {
|
<i class="material-icons">folder</i>
|
||||||
meta.name = name;
|
<div class="folder-name">${escapeHTML(sf.name)}</div>
|
||||||
return meta;
|
</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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
// Enrich each file
|
|
||||||
data.files = data.files.map((f) => {
|
|
||||||
f.fullName = (f.path || f.name).trim().toLowerCase();
|
|
||||||
f.editable = canEditFile(f.name);
|
|
||||||
f.folder = folder;
|
|
||||||
return f;
|
|
||||||
});
|
|
||||||
fileData = data.files;
|
|
||||||
|
|
||||||
// --- folder summary + slider injection ---
|
|
||||||
const actionsContainer = document.getElementById("fileListActions");
|
|
||||||
if (actionsContainer) {
|
|
||||||
// 1) summary
|
|
||||||
let summaryElem = document.getElementById("fileSummary");
|
|
||||||
if (!summaryElem) {
|
|
||||||
summaryElem = document.createElement("div");
|
|
||||||
summaryElem.id = "fileSummary";
|
|
||||||
summaryElem.style.cssText = "float:right; margin:0 60px 0 auto; font-size:0.9em;";
|
|
||||||
actionsContainer.appendChild(summaryElem);
|
|
||||||
}
|
|
||||||
summaryElem.style.display = "block";
|
|
||||||
summaryElem.innerHTML = buildFolderSummary(fileData);
|
|
||||||
|
|
||||||
// 2) view‐mode 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;";
|
|
||||||
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;
|
|
||||||
else if (w < 900) maxCols = 2;
|
|
||||||
else if (w < 1200) maxCols = 4;
|
|
||||||
else maxCols = 6;
|
|
||||||
|
|
||||||
const currentCols = Math.min(
|
|
||||||
parseInt(localStorage.getItem("galleryColumns") || "3", 10),
|
|
||||||
maxCols
|
|
||||||
);
|
|
||||||
|
|
||||||
sliderContainer.innerHTML = `
|
|
||||||
<label for="galleryColumnsSlider" style="margin-right:8px; white-space:nowrap; line-height:1;">
|
|
||||||
${t("columns")}:
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
id="galleryColumnsSlider"
|
|
||||||
min="1"
|
|
||||||
max="${maxCols}"
|
|
||||||
value="${currentCols}"
|
|
||||||
style="vertical-align:middle;"
|
|
||||||
>
|
|
||||||
<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) => {
|
|
||||||
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)`;
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const currentHeight = parseInt(localStorage.getItem("rowHeight") ?? "48", 10);
|
|
||||||
sliderContainer.innerHTML = `
|
|
||||||
<label for="rowHeightSlider" style="margin-right:8px; white-space:nowrap; 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>
|
|
||||||
`;
|
|
||||||
// hookup row‐height slider
|
|
||||||
const rowSlider = document.getElementById("rowHeightSlider");
|
|
||||||
const rowValue = document.getElementById("rowHeightValue");
|
|
||||||
rowSlider.oninput = (e) => {
|
|
||||||
const v = e.target.value;
|
|
||||||
document.documentElement.style.setProperty("--file-row-height", v + "px");
|
|
||||||
localStorage.setItem("rowHeight", v);
|
|
||||||
rowValue.textContent = v + "px";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Render based on viewMode
|
|
||||||
if (window.viewMode === "gallery") {
|
|
||||||
renderGalleryView(folder);
|
|
||||||
} else {
|
} else {
|
||||||
renderFileTable(folder);
|
strip.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFileActionButtons();
|
updateFileActionButtons();
|
||||||
return data.files;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("Error loading file list:", err);
|
|
||||||
if (err !== "Unauthorized") {
|
|
||||||
fileListContainer.textContent = "Error loading files.";
|
|
||||||
}
|
|
||||||
return [];
|
return [];
|
||||||
})
|
}
|
||||||
.finally(() => {
|
|
||||||
fileListContainer.style.visibility = "visible";
|
// 5) normalize files array
|
||||||
|
if (!Array.isArray(data.files)) {
|
||||||
|
data.files = Object.entries(data.files).map(([name, meta]) => {
|
||||||
|
meta.name = name;
|
||||||
|
return meta;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
data.files = data.files.map(f => {
|
||||||
|
f.fullName = (f.path || f.name).trim().toLowerCase();
|
||||||
|
f.editable = canEditFile(f.name);
|
||||||
|
f.folder = folder;
|
||||||
|
return f;
|
||||||
});
|
});
|
||||||
|
fileData = data.files;
|
||||||
|
|
||||||
|
// 6) inject summary + slider
|
||||||
|
if (actionsContainer) {
|
||||||
|
// a) summary
|
||||||
|
let summaryElem = document.getElementById("fileSummary");
|
||||||
|
if (!summaryElem) {
|
||||||
|
summaryElem = document.createElement("div");
|
||||||
|
summaryElem.id = "fileSummary";
|
||||||
|
summaryElem.style.cssText = "float:right; margin:0 60px 0 auto; font-size:0.9em;";
|
||||||
|
actionsContainer.appendChild(summaryElem);
|
||||||
|
}
|
||||||
|
summaryElem.style.display = "block";
|
||||||
|
summaryElem.innerHTML = buildFolderSummary(fileData);
|
||||||
|
|
||||||
|
// 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; margin-right:auto; font-size:0.9em;";
|
||||||
|
actionsContainer.insertBefore(sliderContainer, summaryElem);
|
||||||
|
} else {
|
||||||
|
sliderContainer.style.display = "inline-flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewMode === "gallery") {
|
||||||
|
const w = window.innerWidth;
|
||||||
|
let maxCols;
|
||||||
|
if (w < 600) maxCols = 1;
|
||||||
|
else if (w < 900) maxCols = 2;
|
||||||
|
else if (w < 1200) maxCols = 4;
|
||||||
|
else maxCols = 6;
|
||||||
|
|
||||||
|
const currentCols = Math.min(
|
||||||
|
parseInt(localStorage.getItem("galleryColumns") || "3", 10),
|
||||||
|
maxCols
|
||||||
|
);
|
||||||
|
|
||||||
|
sliderContainer.innerHTML = `
|
||||||
|
<label for="galleryColumnsSlider" style="margin-right:8px;line-height:1;">
|
||||||
|
${t("columns")}:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="galleryColumnsSlider"
|
||||||
|
min="1"
|
||||||
|
max="${maxCols}"
|
||||||
|
value="${currentCols}"
|
||||||
|
style="vertical-align:middle;"
|
||||||
|
>
|
||||||
|
<span id="galleryColumnsValue" style="margin-left:6px;line-height:1;">${currentCols}</span>
|
||||||
|
`;
|
||||||
|
const gallerySlider = document.getElementById("galleryColumnsSlider");
|
||||||
|
const galleryValue = document.getElementById("galleryColumnsValue");
|
||||||
|
gallerySlider.oninput = e => {
|
||||||
|
const v = +e.target.value;
|
||||||
|
localStorage.setItem("galleryColumns", v);
|
||||||
|
galleryValue.textContent = v;
|
||||||
|
document.querySelector(".gallery-container")
|
||||||
|
?.style.setProperty("grid-template-columns", `repeat(${v},1fr)`);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const currentHeight = parseInt(localStorage.getItem("rowHeight") || "48", 10);
|
||||||
|
sliderContainer.innerHTML = `
|
||||||
|
<label for="rowHeightSlider" style="margin-right:8px;line-height:1;">
|
||||||
|
${t("row_height")}:
|
||||||
|
</label>
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
const rowSlider = document.getElementById("rowHeightSlider");
|
||||||
|
const rowValue = document.getElementById("rowHeightValue");
|
||||||
|
rowSlider.oninput = e => {
|
||||||
|
const v = e.target.value;
|
||||||
|
document.documentElement.style.setProperty("--file-row-height", v + "px");
|
||||||
|
localStorage.setItem("rowHeight", v);
|
||||||
|
rowValue.textContent = v + "px";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}" draggable="true">
|
||||||
|
<i class="material-icons">folder</i>
|
||||||
|
<div class="folder-name">${escapeHTML(sf.name)}</div>
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
strip.style.display = "flex";
|
||||||
|
|
||||||
|
// wire up each folder‐tile
|
||||||
|
strip.querySelectorAll(".folder-item").forEach(el => {
|
||||||
|
// 1) click to navigate
|
||||||
|
el.addEventListener("click", () => {
|
||||||
|
const dest = el.dataset.folder;
|
||||||
|
window.currentFolder = dest;
|
||||||
|
localStorage.setItem("lastOpenedFolder", dest);
|
||||||
|
updateBreadcrumbTitle(dest);
|
||||||
|
document.querySelectorAll(".folder-option.selected").forEach(o => o.classList.remove("selected"));
|
||||||
|
document.querySelector(`.folder-option[data-folder="${dest}"]`)?.classList.add("selected");
|
||||||
|
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 {
|
||||||
|
strip.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8) render files
|
||||||
|
if (window.viewMode === "gallery") {
|
||||||
|
renderGalleryView(folder);
|
||||||
|
} else {
|
||||||
|
renderFileTable(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFileActionButtons();
|
||||||
|
return data.files;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading file list:", err);
|
||||||
|
if (err.message !== "Unauthorized") {
|
||||||
|
fileListContainer.textContent = "Error loading files.";
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
fileListContainer.style.visibility = "visible";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update renderFileTable so it writes its content into the provided container.
|
* 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 fileListContent = container || document.getElementById("fileList");
|
||||||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||||||
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
||||||
@@ -408,6 +545,10 @@ export function renderFileTable(folder, container) {
|
|||||||
|
|
||||||
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||||
|
|
||||||
|
fileListContent.querySelectorAll('.folder-item').forEach(el => {
|
||||||
|
el.addEventListener('click', () => loadFileList(el.dataset.folder));
|
||||||
|
});
|
||||||
|
|
||||||
// pagination clicks
|
// pagination clicks
|
||||||
const prevBtn = document.getElementById("prevPageBtn");
|
const prevBtn = document.getElementById("prevPageBtn");
|
||||||
if (prevBtn) prevBtn.addEventListener("click", () => {
|
if (prevBtn) prevBtn.addEventListener("click", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// fileMenu.js
|
// fileMenu.js
|
||||||
import { updateRowHighlight, showToast } from './domUtils.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 { previewFile } from './filePreview.js';
|
||||||
import { editFile } from './fileEditor.js';
|
import { editFile } from './fileEditor.js';
|
||||||
import { canEditFile, fileData } from './fileListView.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);
|
const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value);
|
||||||
|
|
||||||
let menuItems = [
|
let menuItems = [
|
||||||
|
{ label: t("create_file"), action: () => openCreateFileModal() },
|
||||||
{ label: t("delete_selected"), action: () => { handleDeleteSelected(new Event("click")); } },
|
{ label: t("delete_selected"), action: () => { handleDeleteSelected(new Event("click")); } },
|
||||||
{ label: t("copy_selected"), action: () => { handleCopySelected(new Event("click")); } },
|
{ label: t("copy_selected"), action: () => { handleCopySelected(new Event("click")); } },
|
||||||
{ label: t("move_selected"), action: () => { handleMoveSelected(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.
|
// Helper for getting the parent folder.
|
||||||
function getParentFolder(folder) {
|
export function getParentFolder(folder) {
|
||||||
if (folder === "root") return "root";
|
if (folder === "root") return "root";
|
||||||
const lastSlash = folder.lastIndexOf("/");
|
const lastSlash = folder.lastIndexOf("/");
|
||||||
return lastSlash === -1 ? "root" : folder.substring(0, lastSlash);
|
return lastSlash === -1 ? "root" : folder.substring(0, lastSlash);
|
||||||
@@ -361,7 +361,7 @@ function renderBreadcrumbFragment(folderPath) {
|
|||||||
return frag;
|
return frag;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBreadcrumbTitle(folder) {
|
export function updateBreadcrumbTitle(folder) {
|
||||||
const titleEl = document.getElementById("fileListTitle");
|
const titleEl = document.getElementById("fileListTitle");
|
||||||
titleEl.textContent = "";
|
titleEl.textContent = "";
|
||||||
titleEl.appendChild(document.createTextNode(t("files_in") + " ("));
|
titleEl.appendChild(document.createTextNode(t("files_in") + " ("));
|
||||||
@@ -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"),
|
||||||
|
|||||||
@@ -266,7 +266,16 @@ const translations = {
|
|||||||
"items_per_page": "items per page",
|
"items_per_page": "items per page",
|
||||||
"columns": "Columns",
|
"columns": "Columns",
|
||||||
"row_height": "Row Height",
|
"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: {
|
es: {
|
||||||
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
||||||
|
|||||||
@@ -20,6 +20,30 @@ export function initializeApp() {
|
|||||||
window.currentFolder = "root";
|
window.currentFolder = "root";
|
||||||
initTagSearch();
|
initTagSearch();
|
||||||
loadFileList(window.currentFolder);
|
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) {
|
||||||
|
fileListArea.addEventListener('dragover', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
fileListArea.classList.add('drop-hover');
|
||||||
|
});
|
||||||
|
fileListArea.addEventListener('dragleave', () => {
|
||||||
|
fileListArea.classList.remove('drop-hover');
|
||||||
|
});
|
||||||
|
fileListArea.addEventListener('drop', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
fileListArea.classList.remove('drop-hover');
|
||||||
|
// re-dispatch the same drop into the real upload card
|
||||||
|
uploadArea.dispatchEvent(new DragEvent('drop', {
|
||||||
|
dataTransfer: e.dataTransfer,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
initDragAndDrop();
|
initDragAndDrop();
|
||||||
loadSidebarOrder();
|
loadSidebarOrder();
|
||||||
loadHeaderOrder();
|
loadHeaderOrder();
|
||||||
@@ -29,46 +53,37 @@ export function initializeApp() {
|
|||||||
setupTrashRestoreDelete();
|
setupTrashRestoreDelete();
|
||||||
loadAdminConfigFunc();
|
loadAdminConfigFunc();
|
||||||
|
|
||||||
const helpBtn = document.getElementById("folderHelpBtn");
|
const helpBtn = document.getElementById("folderHelpBtn");
|
||||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||||
if (helpBtn && helpTooltip) {
|
if (helpBtn && helpTooltip) {
|
||||||
helpBtn.addEventListener("click", () => {
|
helpBtn.addEventListener("click", () => {
|
||||||
helpTooltip.style.display =
|
helpTooltip.style.display =
|
||||||
helpTooltip.style.display === "block" ? "none" : "block";
|
helpTooltip.style.display === "block" ? "none" : "block";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +100,8 @@ export function triggerLogout() {
|
|||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "X-CSRF-Token": window.csrfToken }
|
headers: { "X-CSRF-Token": window.csrfToken }
|
||||||
})
|
})
|
||||||
.then(() => window.location.reload(true))
|
.then(() => window.location.reload(true))
|
||||||
.catch(()=>{});
|
.catch(() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -120,10 +135,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
// Continue with initializations that rely on a valid CSRF token:
|
// Continue with initializations that rely on a valid CSRF token:
|
||||||
checkAuthentication().then(authenticated => {
|
checkAuthentication().then(authenticated => {
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
const overlay = document.getElementById('loadingOverlay');
|
const overlay = document.getElementById('loadingOverlay');
|
||||||
if (overlay) overlay.remove();
|
if (overlay) overlay.remove();
|
||||||
initializeApp();
|
initializeApp();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Other DOM initialization that can happen after CSRF is ready.
|
// Other DOM initialization that can happen after CSRF is ready.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -669,6 +669,18 @@ function submitFiles(allFiles) {
|
|||||||
}
|
}
|
||||||
allSucceeded = false;
|
allSucceeded = false;
|
||||||
}
|
}
|
||||||
|
if (file.isClipboard) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.selectedFiles = [];
|
||||||
|
updateFileInfoCount();
|
||||||
|
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||||
|
if (progressContainer) progressContainer.innerHTML = "";
|
||||||
|
const fileInfoContainer = document.getElementById("fileInfoContainer");
|
||||||
|
if (fileInfoContainer) {
|
||||||
|
fileInfoContainer.innerHTML = `<span id="fileInfoDefault">No files selected</span>`;
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Only now count this chunk as finished ───────────────────
|
// ─── Only now count this chunk as finished ───────────────────
|
||||||
finishedCount++;
|
finishedCount++;
|
||||||
@@ -847,4 +859,39 @@ function initUpload() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { initUpload };
|
export { initUpload };
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Clipboard Paste Handler (Mimics Drag-and-Drop)
|
||||||
|
// -------------------------
|
||||||
|
document.addEventListener('paste', function handlePasteUpload(e) {
|
||||||
|
const items = e.clipboardData?.items;
|
||||||
|
if (!items) return;
|
||||||
|
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
|
if (item.kind === 'file') {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (file) {
|
||||||
|
const ext = file.name.split('.').pop() || 'png';
|
||||||
|
const renamedFile = new File([file], `image${Date.now()}.${ext}`, { type: file.type });
|
||||||
|
renamedFile.isClipboard = true;
|
||||||
|
|
||||||
|
Object.defineProperty(renamedFile, 'customRelativePath', {
|
||||||
|
value: renamedFile.name,
|
||||||
|
writable: true,
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
files.push(renamedFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
processFiles(files);
|
||||||
|
showToast('Pasted file added to upload list.', 'success');
|
||||||
|
}
|
||||||
|
});
|
||||||
63
scripts/scan_uploads.php
Normal file
63
scripts/scan_uploads.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* scan_uploads.php
|
||||||
|
* Scans the uploads directory and creates metadata entries for new files/folders using config settings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config/config.php';
|
||||||
|
|
||||||
|
if (!isset($config['upload_dir']) || !isset($config['metadata_dir'])) {
|
||||||
|
die("Missing configuration for upload_dir or metadata_dir\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadDir = $config['upload_dir'];
|
||||||
|
$metadataDir = $config['metadata_dir'];
|
||||||
|
date_default_timezone_set('UTC');
|
||||||
|
|
||||||
|
function scanDirectory($dir) {
|
||||||
|
$items = array_diff(scandir($dir), ['.', '..']);
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$path = $dir . DIRECTORY_SEPARATOR . $item;
|
||||||
|
$results[] = $path;
|
||||||
|
|
||||||
|
if (is_dir($path)) {
|
||||||
|
$results = array_merge($results, scanDirectory($path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metadataPath($filePath, $uploadDir, $metadataDir) {
|
||||||
|
$relativePath = ltrim(str_replace($uploadDir, '', $filePath), '/');
|
||||||
|
return $metadataDir . '/' . $relativePath . '.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
$allItems = scanDirectory($uploadDir);
|
||||||
|
|
||||||
|
foreach ($allItems as $item) {
|
||||||
|
$metaPath = metadataPath($item, $uploadDir, $metadataDir);
|
||||||
|
|
||||||
|
if (!file_exists($metaPath)) {
|
||||||
|
$type = is_dir($item) ? 'folder' : 'file';
|
||||||
|
$size = is_file($item) ? filesize($item) : 0;
|
||||||
|
|
||||||
|
$metadata = [
|
||||||
|
'path' => str_replace($uploadDir, '', $item),
|
||||||
|
'type' => $type,
|
||||||
|
'size' => $size,
|
||||||
|
'user' => 'Imported',
|
||||||
|
'uploadDate' => date('c')
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!is_dir(dirname($metaPath))) {
|
||||||
|
mkdir(dirname($metaPath), 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($metaPath, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||||
|
echo "Created metadata for: {$item}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -1626,4 +1626,31 @@ class FileController
|
|||||||
echo json_encode(['success' => false, 'error' => 'Not found']);
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,12 +96,14 @@ class FolderController
|
|||||||
|
|
||||||
// Basic sanitation for folderName.
|
// Basic sanitation for folderName.
|
||||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||||
|
http_response_code(400);
|
||||||
echo json_encode(['error' => 'Invalid folder name.']);
|
echo json_encode(['error' => 'Invalid folder name.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally sanitize the parent.
|
// Optionally sanitize the parent.
|
||||||
if ($parent && !preg_match(REGEX_FOLDER_NAME, $parent)) {
|
if ($parent && !preg_match(REGEX_FOLDER_NAME, $parent)) {
|
||||||
|
http_response_code(400);
|
||||||
echo json_encode(['error' => 'Invalid parent folder name.']);
|
echo json_encode(['error' => 'Invalid parent folder name.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -340,16 +342,14 @@ class FolderController
|
|||||||
public function getFolderList(): void
|
public function getFolderList(): void
|
||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
if (empty($_SESSION['authenticated'])) {
|
||||||
// Ensure user is authenticated.
|
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally, you might add further input validation if necessary.
|
$parent = $_GET['folder'] ?? null;
|
||||||
$folderList = FolderModel::getFolderList();
|
$folderList = FolderModel::getFolderList($parent);
|
||||||
echo json_encode($folderList);
|
echo json_encode($folderList);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -1087,11 +1087,11 @@ class FolderController
|
|||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
$shareFile = META_DIR . 'share_folder_links.json';
|
$shareFile = META_DIR . 'share_folder_links.json';
|
||||||
$links = file_exists($shareFile)
|
$links = file_exists($shareFile)
|
||||||
? json_decode(file_get_contents($shareFile), true) ?? []
|
? json_decode(file_get_contents($shareFile), true) ?? []
|
||||||
: [];
|
: [];
|
||||||
$now = time();
|
$now = time();
|
||||||
$cleaned = [];
|
$cleaned = [];
|
||||||
|
|
||||||
// 1) Remove expired
|
// 1) Remove expired
|
||||||
foreach ($links as $token => $record) {
|
foreach ($links as $token => $record) {
|
||||||
if (!empty($record['expires']) && $record['expires'] < $now) {
|
if (!empty($record['expires']) && $record['expires'] < $now) {
|
||||||
@@ -1099,12 +1099,12 @@ class FolderController
|
|||||||
}
|
}
|
||||||
$cleaned[$token] = $record;
|
$cleaned[$token] = $record;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Persist back if anything was pruned
|
// 2) Persist back if anything was pruned
|
||||||
if (count($cleaned) !== count($links)) {
|
if (count($cleaned) !== count($links)) {
|
||||||
file_put_contents($shareFile, json_encode($cleaned, JSON_PRETTY_PRINT));
|
file_put_contents($shareFile, json_encode($cleaned, JSON_PRETTY_PRINT));
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode($cleaned);
|
echo json_encode($cleaned);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1278,4 +1278,64 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
|||||||
file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT));
|
file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT));
|
||||||
return true;
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
7
start.sh
7
start.sh
@@ -109,4 +109,9 @@ if [ ! -f /var/www/metadata/createdTags.json ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "🔥 Starting Apache..."
|
echo "🔥 Starting Apache..."
|
||||||
exec apachectl -D FOREGROUND
|
exec apachectl -D FOREGROUND
|
||||||
|
|
||||||
|
if [ "$SCAN_ON_START" = "true" ]; then
|
||||||
|
echo "Scanning uploads directory to generate metadata..."
|
||||||
|
php /var/www/scripts/scan_uploads.php
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user