Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a9d332d60 | ||
|
|
1983f7705f | ||
|
|
6b2bf0ba70 | ||
|
|
6d9715169c | ||
|
|
0645a3712a | ||
|
|
ebc32ea965 |
78
CHANGELOG.md
78
CHANGELOG.md
@@ -1,5 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 4/26/2025 1.2.6
|
||||
|
||||
**Apache / Dockerfile (CSP)**
|
||||
|
||||
- Enabled Apache’s `mod_headers` in the Dockerfile (`a2enmod headers ssl deflate expires proxy proxy_fcgi rewrite`)
|
||||
- Added a strong `Content-Security-Policy` header in the vhost configs to lock down allowed sources for scripts, styles, fonts, images, and connections
|
||||
|
||||
**index.html & CDN Includes**
|
||||
|
||||
- Applied Subresource Integrity (`integrity` + `crossorigin="anonymous"`) to all static CDN assets (Bootstrap CSS, CodeMirror CSS/JS, Resumable.js, DOMPurify, Fuse.js)
|
||||
- Omitted SRI on Google Fonts & Material Icons links (dynamic per-browser CSS)
|
||||
- Removed all inline `<script>` and `onclick` attributes; now all behaviors live in external JS modules
|
||||
|
||||
**auth.js (Logout Handling)**
|
||||
|
||||
- Moved the logout-on-`?logout=1` snippet from inline HTML into `auth.js`
|
||||
- In `DOMContentLoaded`, attached a `click` listener to `#logoutBtn` that POSTs to `/api/auth/logout.php` and reloads
|
||||
|
||||
**fileActions.js (Modal Button Handlers)**
|
||||
|
||||
- Externalized the cancel/download buttons for single-file and ZIP-download modals by adding `click` listeners in `fileActions.js`
|
||||
- Removed the inline `onclick` attributes from `#cancelDownloadFile` and `#confirmSingleDownloadButton` in the HTML
|
||||
- Ensured all file-action modals (delete, download, extract, copy, move, rename) now use JS event handlers instead of inline code
|
||||
|
||||
**domUtils.js**
|
||||
|
||||
- **Removed** all inline `onclick` and `onchange` attributes from:
|
||||
- `buildSearchAndPaginationControls` (advanced search toggle, prev/next buttons, items-per-page selector)
|
||||
- `buildFileTableHeader` (select-all checkbox)
|
||||
- `buildFileTableRow` (download, edit, preview, rename buttons)
|
||||
- **Retained** all original logic (file-type icon detection, shift-select, debounce, custom confirm modal, etc.)
|
||||
|
||||
**fileListView.js**
|
||||
|
||||
- **Stopped** generating inline `onclick` handlers in both table and gallery views.
|
||||
- **Added** `data-` attributes on actionable elements:
|
||||
- `data-download-name`, `data-download-folder`
|
||||
- `data-edit-name`, `data-edit-folder`
|
||||
- `data-rename-name`, `data-rename-folder`
|
||||
- `data-preview-url`, `data-preview-name`
|
||||
- IDs on controls: `#advancedSearchToggle`, `#searchInput`, `#prevPageBtn`, `#nextPageBtn`, `#selectAll`, `#itemsPerPageSelect`
|
||||
- **Introduced** `attachListControlListeners()` to bind all events via `addEventListener` immediately after rendering, preserving every interaction without inline code.
|
||||
|
||||
**Additional changes**
|
||||
|
||||
- **Security**: Added `frame-src 'self'` to the Content-Security-Policy header so that the embedded API docs iframe can load from our own origin without relaxing JS restrictions.
|
||||
- **Controller**: Updated `FolderController::shareFolder()` (folderController) to include the gallery-view toggle script block intact, ensuring the “Switch to Gallery View” button works when sharing folders.
|
||||
- **UI (fileListView.js)**: Refactored `renderGalleryView` to remove all inline `onclick=` handlers; switched to using data-attributes and `addEventListener()` for preview, download, edit and rename buttons, fully CSP-compliant.
|
||||
- Moved logout button handler out of inline `<script>` in `index.html` and into the `DOMContentLoaded` init in **main.js** (via `auth.js`), so it now attaches reliably after the CSRF token is loaded and DOM is ready.
|
||||
|
||||
---
|
||||
|
||||
## Changes 4/25/2025
|
||||
|
||||
- Switch single‐file download to native `<a>` link (no JS buffering)
|
||||
- Keep spinner modal during ZIP creation and download blob on POST response
|
||||
- Replace text toggle with a single button showing sun/moon icons and hover tooltip
|
||||
|
||||
## Changes 4/24/2025 1.2.5
|
||||
|
||||
- Enhance README and wiki with expanded installation instructions
|
||||
@@ -10,13 +68,27 @@
|
||||
- Enable `mod_deflate` compression for HTML, plain text, CSS, JS and JSON
|
||||
- Configure `mod_expires` caching for images (1 month), CSS (1 week) and JS (3 hour)
|
||||
- Deny access to hidden files (dot-files)
|
||||
- Add access control in public/.htaccess for api.html & openapi.json; update Nginx example in wiki
|
||||
~~- Add access control in public/.htaccess for api.html & openapi.json; update Nginx example in wiki~~
|
||||
- Remove obsolete folders from repo root
|
||||
- Embed API documentation (`api.html`) directly in the FileRise UI as a full-screen modal
|
||||
- Embed API documentation (`api.php`) directly in the FileRise UI as a full-screen modal
|
||||
- Introduced `openApiModalBtn` in the user panel to launch the API modal
|
||||
- Added `#apiModal` container with a same-origin `<iframe src="api.html">` so session cookies authenticate automatically
|
||||
- Added `#apiModal` container with a same-origin `<iframe src="api.php">` so session cookies authenticate automatically
|
||||
- Close control uses the existing `.editor-close-btn` for consistent styling and hover effects
|
||||
|
||||
- public/api.html has been replaced by the new api.php wrapper
|
||||
- **`public/api.php`**
|
||||
- Single PHP endpoint for both UI and spec
|
||||
- Enforces `$_SESSION['authenticated']`
|
||||
- Renders the Redoc API docs when accessed normally
|
||||
- Streams the JSON spec from `openapi.json.dist` when called as `api.php?spec=1`
|
||||
- Redirects unauthenticated users to `index.html?redirect=/api.php`
|
||||
- **Moved** `public/openapi.json` → `openapi.json.dist` (moved outside of `public/`) to prevent direct static access
|
||||
- **Dockerfile**: enabled required Apache modules for rewrite, security headers, proxying, caching and compression:
|
||||
|
||||
```dockerfile
|
||||
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate
|
||||
```
|
||||
|
||||
## Changes 4/23/2025 1.2.4
|
||||
|
||||
**AuthModel**
|
||||
|
||||
@@ -78,6 +78,7 @@ RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self'; frame-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||
</IfModule>
|
||||
|
||||
# Compression
|
||||
@@ -126,7 +127,7 @@ RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
||||
EOF
|
||||
|
||||
# Enable required modules
|
||||
RUN a2enmod rewrite headers
|
||||
RUN a2enmod rewrite headers proxy proxy_fcgi expires deflate
|
||||
|
||||
EXPOSE 80 443
|
||||
COPY start.sh /usr/local/bin/start.sh
|
||||
|
||||
@@ -15,10 +15,6 @@ DirectoryIndex index.html
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "^(api\.html|openapi\.json)$">
|
||||
Require valid-user
|
||||
</FilesMatch>
|
||||
|
||||
# -----------------------------
|
||||
# Enforce HTTPS (optional)
|
||||
# -----------------------------
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<!-- public/api.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>FileRise API Docs</title>
|
||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js" integrity="sha384-4vOjrBu7SuDWXcAw1qFznVLA/sKL+0l4nn+J1HY8w7cpa6twQEYuh4b0Cwuo7CyX" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="openapi.json"></redoc>
|
||||
<div id="redoc-container"></div>
|
||||
<script>
|
||||
// If the <redoc> tag didn’t render, fall back to init()
|
||||
if (!customElements.get('redoc')) {
|
||||
Redoc.init('openapi.json', {}, document.getElementById('redoc-container'));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
35
public/api.php
Normal file
35
public/api.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
// public/api.php
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
header('Location: /index.html?redirect=/api.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['spec'])) {
|
||||
header('Content-Type: application/json');
|
||||
readfile(__DIR__ . '/../openapi.json.dist');
|
||||
exit;
|
||||
}
|
||||
|
||||
?><!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>FileRise API Docs</title>
|
||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"
|
||||
integrity="sha384-4vOjrBu7SuDWXcAw1qFznVLA/sKL+0l4nn+J1HY8w7cpa6twQEYuh4b0Cwuo7CyX"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="api.php?spec=1"></redoc>
|
||||
<div id="redoc-container"></div>
|
||||
<script>
|
||||
if (!customElements.get('redoc')) {
|
||||
Redoc.init('api.php?spec=1', {}, document.getElementById('redoc-container'));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -80,6 +80,9 @@ body.dark-mode .header-container {
|
||||
background-color: #1f1f1f;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
#darkModeIcon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
max-height: 50px;
|
||||
|
||||
@@ -5,13 +5,6 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title data-i18n-key="title">FileRise</title>
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('logout') === '1') {
|
||||
localStorage.removeItem("username");
|
||||
localStorage.removeItem("userTOTPEnabled");
|
||||
}
|
||||
</script>
|
||||
<link rel="icon" type="image/png" href="/assets/logo.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
||||
<meta name="csrf-token" content="">
|
||||
@@ -20,9 +13,12 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
|
||||
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css"
|
||||
integrity="sha384-zaeBlB/vwYsDRSlFajnDd7OydJ0cWk+c2OWybl3eSUf6hW2EbhlCsQPqKr3gkznT" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css"
|
||||
integrity="sha384-eZTPTN0EvJdn23s24UDYJmUM2T7C2ZFa3qFLypeBruJv8mZeTusKUAO/j5zPAQ6l" crossorigin="anonymous">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"
|
||||
integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma"
|
||||
crossorigin="anonymous"></script>
|
||||
@@ -41,9 +37,9 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
||||
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"
|
||||
integrity="sha384-zPE55eyESN+FxCWGEnlNxGyAPJud6IZ6TtJmXb56OFRGhxZPN4akj9rjA3gw5Qqa"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"
|
||||
integrity="sha384-zPE55eyESN+FxCWGEnlNxGyAPJud6IZ6TtJmXb56OFRGhxZPN4akj9rjA3gw5Qqa"
|
||||
crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="css/styles.css" />
|
||||
</head>
|
||||
|
||||
@@ -78,16 +74,16 @@
|
||||
stroke: white;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
|
||||
.divider {
|
||||
stroke: #1565C0;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
|
||||
.drawer {
|
||||
fill: #FFFFFF;
|
||||
}
|
||||
|
||||
|
||||
.handle {
|
||||
fill: #1565C0;
|
||||
}
|
||||
@@ -159,7 +155,11 @@
|
||||
<button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;">
|
||||
<i class="material-icons">person_remove</i>
|
||||
</button>
|
||||
<button id="darkModeToggle" class="dark-mode-toggle" data-i18n-key="dark_mode_toggle">Dark Mode</button>
|
||||
<button id="darkModeToggle" class="btn-icon" aria-label="Toggle dark mode">
|
||||
<span class="material-icons" id="darkModeIcon">
|
||||
dark_mode
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,7 +200,8 @@
|
||||
</div>
|
||||
<!-- Basic HTTP Login Option -->
|
||||
<div class="text-center mt-3">
|
||||
<a href="/api/auth/login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic HTTP
|
||||
<a href="/api/auth/login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic
|
||||
HTTP
|
||||
Login</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,10 +285,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button id="shareFolderBtn" class="btn btn-secondary ml-2" data-i18n-title="share_folder">
|
||||
<i class="material-icons">share</i>
|
||||
</button>
|
||||
</button>
|
||||
<button id="deleteFolderBtn" class="btn btn-danger ml-2" data-i18n-title="delete_folder">
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
@@ -391,36 +392,43 @@
|
||||
</div> <!-- end mainColumn -->
|
||||
</div> <!-- end main-wrapper -->
|
||||
|
||||
<!-- Download Progress Modal -->
|
||||
<div id="downloadProgressModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
<!-- Material icon spinner with a dedicated class -->
|
||||
<span class="material-icons download-spinner">autorenew</span>
|
||||
<p data-i18n-key="preparing_download">Preparing your download...</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Download Progress Modal -->
|
||||
<div id="downloadProgressModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
<h4 id="downloadProgressTitle" data-i18n-key="preparing_download">
|
||||
Preparing your download...
|
||||
</h4>
|
||||
|
||||
<!-- Single File Download Modal -->
|
||||
<div id="downloadFileModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
<h4 data-i18n-key="download_file">Download File</h4>
|
||||
<p data-i18n-key="confirm_or_change_filename">Confirm or change the download file name:</p>
|
||||
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename" placeholder="Filename" />
|
||||
<div style="margin-top: 15px; text-align: right;">
|
||||
<button id="cancelDownloadFile" class="btn btn-secondary"
|
||||
onclick="document.getElementById('downloadFileModal').style.display = 'none';"
|
||||
data-i18n-key="cancel">Cancel</button>
|
||||
<button id="confirmSingleDownloadButton" class="btn btn-primary"
|
||||
onclick="confirmSingleDownload()"
|
||||
data-i18n-key="download">Download</button>
|
||||
<!-- spinner -->
|
||||
<span class="material-icons download-spinner">autorenew</span>
|
||||
|
||||
<!-- these were missing -->
|
||||
<progress id="downloadProgressBar" value="0" max="100" style="width:100%; height:1.5em; display:none;"></progress>
|
||||
<p>
|
||||
<span id="downloadProgressPercent" style="display:none;">0%</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Single File Download Modal -->
|
||||
<div id="downloadFileModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
<h4 data-i18n-key="download_file">Download File</h4>
|
||||
<p data-i18n-key="confirm_or_change_filename">Confirm or change the download file name:</p>
|
||||
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename"
|
||||
placeholder="Filename" />
|
||||
<div style="margin-top: 15px; text-align: right;">
|
||||
<button id="cancelDownloadFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
||||
<button id="confirmSingleDownloadButton" class="btn btn-primary" data-i18n-key="download">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</span>
|
||||
<span id="closeChangePasswordModal"
|
||||
style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</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;" />
|
||||
|
||||
@@ -437,13 +437,7 @@ function initAuth() {
|
||||
submitLogin(formData);
|
||||
});
|
||||
}
|
||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
||||
fetch("/api/auth/logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
}).then(() => window.location.reload(true)).catch(() => { });
|
||||
});
|
||||
|
||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||
resetUserForm();
|
||||
toggleVisibility("addUserModal", true);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
import { loadAdminConfigFunc } from './auth.js';
|
||||
|
||||
const version = "v1.2.5"; // Update this version string as needed
|
||||
const version = "v1.2.6"; // Update this version string as needed
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||
|
||||
let lastLoginData = null;
|
||||
@@ -245,12 +245,15 @@ export function openUserPanel() {
|
||||
background: rgba(0,0,0,0.8); z-index: 4000; display:none;
|
||||
align-items: center; justify-content: center;
|
||||
`;
|
||||
|
||||
// api.php
|
||||
apiModal.innerHTML = `
|
||||
<div style="position:relative; width:90vw; height:90vh; background:#fff; border-radius:8px; overflow:hidden;">
|
||||
<div class="editor-close-btn" id="closeApiModal">×</div>
|
||||
<iframe src="api.html" style="width:100%;height:100%;border:none;"></iframe>
|
||||
<iframe src="api.php" style="width:100%;height:100%;border:none;"></iframe>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(apiModal);
|
||||
|
||||
document.getElementById("openApiModalBtn").addEventListener("click", () => {
|
||||
|
||||
@@ -91,7 +91,7 @@ export function showToast(message, duration = 3000) {
|
||||
export function buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm }) {
|
||||
const safeSearchTerm = escapeHTML(searchTerm);
|
||||
// Choose the placeholder text based on advanced search mode
|
||||
const placeholderText = window.advancedSearchEnabled
|
||||
const placeholderText = window.advancedSearchEnabled
|
||||
? t("search_placeholder_advanced")
|
||||
: t("search_placeholder");
|
||||
|
||||
@@ -101,7 +101,7 @@ export function buildSearchAndPaginationControls({ currentPage, totalPages, sear
|
||||
<div class="input-group">
|
||||
<!-- Advanced Search Toggle Button -->
|
||||
<div class="input-group-prepend">
|
||||
<button id="advancedSearchToggle" class="btn btn-outline-secondary btn-icon" onclick="toggleAdvancedSearch()" title="${window.advancedSearchEnabled ? t("basic_search_tooltip") : t("advanced_search_tooltip")}">
|
||||
<button id="advancedSearchToggle" class="btn btn-outline-secondary btn-icon" title="${window.advancedSearchEnabled ? t("basic_search_tooltip") : t("advanced_search_tooltip")}">
|
||||
<i class="material-icons">${window.advancedSearchEnabled ? "filter_alt_off" : "filter_alt"}</i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -117,9 +117,9 @@ export function buildSearchAndPaginationControls({ currentPage, totalPages, sear
|
||||
</div>
|
||||
<div class="col-12 col-md-4 text-left">
|
||||
<div class="d-flex justify-content-center justify-content-md-start align-items-center">
|
||||
<button class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""} onclick="changePage(${currentPage - 1})">${t("prev")}</button>
|
||||
<button id="prevPageBtn" class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""}>${t("prev")}</button>
|
||||
<span class="page-indicator">${t("page")} ${currentPage} ${t("of")} ${totalPages || 1}</span>
|
||||
<button class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""} onclick="changePage(${currentPage + 1})">${t("next")}</button>
|
||||
<button id="nextPageBtn" class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""}>${t("next")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +131,7 @@ export function buildFileTableHeader(sortOrder) {
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="checkbox-col"><input type="checkbox" id="selectAll" onclick="toggleAllCheckboxes(this)"></th>
|
||||
<th class="checkbox-col"><input type="checkbox" id="selectAll"></th>
|
||||
<th data-column="name" class="sortable-col">${t("file_name")} ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
<th data-column="modified" class="hide-small sortable-col">${t("date_modified")} ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
<th data-column="uploaded" class="hide-small hide-medium sortable-col">${t("upload_date")} ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
@@ -162,15 +162,15 @@ export function buildFileTableRow(file, folderPath) {
|
||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||
previewIcon = `<i class="material-icons">audiotrack</i>`;
|
||||
}
|
||||
previewButton = `<button class="btn btn-sm btn-info preview-btn" onclick="event.stopPropagation(); previewFile('${folderPath + encodeURIComponent(file.name)}', '${safeFileName}')">
|
||||
previewButton = `<button class="btn btn-sm btn-info preview-btn" data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}" data-preview-name="${safeFileName}">
|
||||
${previewIcon}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<tr onclick="toggleRowSelection(event, '${safeFileName}')" class="clickable-row">
|
||||
<tr class="clickable-row">
|
||||
<td>
|
||||
<input type="checkbox" class="file-checkbox" value="${safeFileName}" onclick="event.stopPropagation(); updateRowHighlight(this);">
|
||||
<input type="checkbox" class="file-checkbox" value="${safeFileName}">
|
||||
</td>
|
||||
<td class="file-name-cell">${safeFileName}</td>
|
||||
<td class="hide-small nowrap">${safeModified}</td>
|
||||
@@ -179,22 +179,16 @@ export function buildFileTableRow(file, folderPath) {
|
||||
<td class="hide-small hide-medium nowrap">${safeUploader}</td>
|
||||
<td>
|
||||
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
|
||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
||||
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
|
||||
title="${t('download')}">
|
||||
<button type="button" class="btn btn-sm btn-success download-btn" data-download-name="${file.name}" data-download-folder="${file.folder || 'root'}" title="${t('download')}">
|
||||
<i class="material-icons">file_download</i>
|
||||
</button>
|
||||
${file.editable ? `
|
||||
<button class="btn btn-sm edit-btn"
|
||||
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
title="${t('edit')}">
|
||||
<button class="btn btn-sm edit-btn" data-edit-name="${file.name}" data-edit-folder="${file.folder || 'root'}" title="${t('edit')}">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>
|
||||
` : ""}
|
||||
${previewButton}
|
||||
<button class="btn btn-sm btn-warning rename-btn"
|
||||
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
title="${t('rename')}">
|
||||
<button class="btn btn-sm btn-warning rename-btn" data-rename-name="${file.name}" data-rename-folder="${file.folder || 'root'}" title="${t('rename')}">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -207,10 +201,10 @@ export function buildBottomControls(itemsPerPageSetting) {
|
||||
return `
|
||||
<div class="d-flex align-items-center mt-3 bottom-controls">
|
||||
<label class="label-inline mr-2 mb-0">${t("show")}</label>
|
||||
<select class="form-control bottom-select" onchange="changeItemsPerPage(this.value)">
|
||||
<select class="form-control bottom-select" id="itemsPerPageSelect">
|
||||
${[10, 20, 50, 100]
|
||||
.map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`)
|
||||
.join("")}
|
||||
.map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`)
|
||||
.join("")}
|
||||
</select>
|
||||
<span class="items-per-page-text ml-2 mb-0">${t("items_per_page")}</span>
|
||||
</div>
|
||||
@@ -345,4 +339,7 @@ export function showCustomConfirmModal(message) {
|
||||
yesBtn.addEventListener("click", onYes);
|
||||
noBtn.addEventListener("click", onNo);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.toggleRowSelection = toggleRowSelection;
|
||||
window.updateRowHighlight = updateRowHighlight;
|
||||
@@ -80,16 +80,16 @@ export function openDownloadModal(fileName, folder) {
|
||||
// Store file details globally for the download confirmation function.
|
||||
window.singleFileToDownload = fileName;
|
||||
window.currentFolder = folder || "root";
|
||||
|
||||
|
||||
// Optionally pre-fill the file name input in the modal.
|
||||
const input = document.getElementById("downloadFileNameInput");
|
||||
if (input) {
|
||||
input.value = fileName; // Use file name as-is (or modify if desired)
|
||||
}
|
||||
|
||||
|
||||
// Show the single file download modal (a new modal element).
|
||||
document.getElementById("downloadFileModal").style.display = "block";
|
||||
|
||||
|
||||
// Optionally focus the input after a short delay.
|
||||
setTimeout(() => {
|
||||
if (input) input.focus();
|
||||
@@ -97,58 +97,34 @@ export function openDownloadModal(fileName, folder) {
|
||||
}
|
||||
|
||||
export function confirmSingleDownload() {
|
||||
// Get the file name from the modal. Users can change it if desired.
|
||||
let fileName = document.getElementById("downloadFileNameInput").value.trim();
|
||||
// 1) Get and validate the filename
|
||||
const input = document.getElementById("downloadFileNameInput");
|
||||
const fileName = input.value.trim();
|
||||
if (!fileName) {
|
||||
showToast("Please enter a name for the file.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide the download modal.
|
||||
|
||||
// 2) Hide the download-name modal
|
||||
document.getElementById("downloadFileModal").style.display = "none";
|
||||
// Show the progress modal (same as in your ZIP download flow).
|
||||
document.getElementById("downloadProgressModal").style.display = "block";
|
||||
|
||||
// Build the URL for download.php using GET parameters.
|
||||
|
||||
// 3) Build the direct download URL
|
||||
const folder = window.currentFolder || "root";
|
||||
const downloadURL = "/api/file/download.php?folder=" + encodeURIComponent(folder) +
|
||||
"&file=" + encodeURIComponent(window.singleFileToDownload);
|
||||
|
||||
fetch(downloadURL, {
|
||||
method: "GET",
|
||||
credentials: "include"
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error("Failed to download file: " + text);
|
||||
});
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
if (!blob || blob.size === 0) {
|
||||
throw new Error("Received empty file.");
|
||||
}
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
// Hide the progress modal.
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
showToast("Download started.");
|
||||
})
|
||||
.catch(error => {
|
||||
// Hide progress modal and show error.
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
console.error("Error downloading file:", error);
|
||||
showToast("Error downloading file: " + error.message);
|
||||
});
|
||||
const downloadURL = "/api/file/download.php"
|
||||
+ "?folder=" + encodeURIComponent(folder)
|
||||
+ "&file=" + encodeURIComponent(window.singleFileToDownload);
|
||||
|
||||
// 4) Trigger native browser download
|
||||
const a = document.createElement("a");
|
||||
a.href = downloadURL;
|
||||
a.download = fileName;
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
// 5) Notify the user
|
||||
showToast("Download started. Check your browser’s download manager.");
|
||||
}
|
||||
|
||||
export function handleExtractZipSelected(e) {
|
||||
@@ -168,16 +144,21 @@ export function handleExtractZipSelected(e) {
|
||||
showToast("No zip files selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Change progress modal text to "Extracting files..."
|
||||
const progressText = document.querySelector("#downloadProgressModal p");
|
||||
if (progressText) {
|
||||
progressText.textContent = "Extracting files...";
|
||||
}
|
||||
|
||||
// Show the progress modal.
|
||||
document.getElementById("downloadProgressModal").style.display = "block";
|
||||
|
||||
|
||||
// Prepare and show the spinner-only modal
|
||||
const modal = document.getElementById("downloadProgressModal");
|
||||
const titleEl = document.getElementById("downloadProgressTitle");
|
||||
const spinner = modal.querySelector(".download-spinner");
|
||||
const progressBar = document.getElementById("downloadProgressBar");
|
||||
const progressPct = document.getElementById("downloadProgressPercent");
|
||||
|
||||
if (titleEl) titleEl.textContent = "Extracting files…";
|
||||
if (spinner) spinner.style.display = "inline-block";
|
||||
if (progressBar) progressBar.style.display = "none";
|
||||
if (progressPct) progressPct.style.display = "none";
|
||||
|
||||
modal.style.display = "block";
|
||||
|
||||
fetch("/api/file/extractZip.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
@@ -192,45 +173,42 @@ export function handleExtractZipSelected(e) {
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Hide the progress modal once the request has completed.
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
modal.style.display = "none";
|
||||
if (data.success) {
|
||||
let toastMessage = "Zip file(s) extracted successfully!";
|
||||
if (data.extractedFiles && Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
|
||||
toastMessage = "Extracted: " + data.extractedFiles.join(", ");
|
||||
let msg = "Zip file(s) extracted successfully!";
|
||||
if (Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
|
||||
msg = "Extracted: " + data.extractedFiles.join(", ");
|
||||
}
|
||||
showToast(toastMessage);
|
||||
showToast(msg);
|
||||
loadFileList(window.currentFolder);
|
||||
} else {
|
||||
showToast("Error extracting zip: " + (data.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Hide the progress modal on error.
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
modal.style.display = "none";
|
||||
console.error("Error extracting zip files:", error);
|
||||
showToast("Error extracting zip files.");
|
||||
});
|
||||
}
|
||||
|
||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||
if (extractZipBtn) {
|
||||
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
|
||||
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const zipNameModal = document.getElementById("downloadZipModal");
|
||||
const progressModal = document.getElementById("downloadProgressModal");
|
||||
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
||||
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const cancelDownloadZip = document.getElementById("cancelDownloadZip");
|
||||
if (cancelDownloadZip) {
|
||||
cancelDownloadZip.addEventListener("click", function () {
|
||||
document.getElementById("downloadZipModal").style.display = "none";
|
||||
// 1) Cancel button hides the name modal
|
||||
if (cancelZipBtn) {
|
||||
cancelZipBtn.addEventListener("click", () => {
|
||||
zipNameModal.style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
// This part remains in your confirmDownloadZip event handler:
|
||||
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
|
||||
if (confirmDownloadZip) {
|
||||
confirmDownloadZip.addEventListener("click", function () {
|
||||
// 2) Confirm button kicks off the zip+download
|
||||
if (confirmZipBtn) {
|
||||
confirmZipBtn.addEventListener("click", async () => {
|
||||
// a) Validate ZIP filename
|
||||
let zipName = document.getElementById("zipFileNameInput").value.trim();
|
||||
if (!zipName) {
|
||||
showToast("Please enter a name for the zip file.");
|
||||
@@ -239,52 +217,56 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (!zipName.toLowerCase().endsWith(".zip")) {
|
||||
zipName += ".zip";
|
||||
}
|
||||
// Hide the ZIP name input modal
|
||||
document.getElementById("downloadZipModal").style.display = "none";
|
||||
// Show the progress modal here only on confirm
|
||||
console.log("Download confirmed. Showing progress modal.");
|
||||
document.getElementById("downloadProgressModal").style.display = "block";
|
||||
const folder = window.currentFolder || "root";
|
||||
fetch("/api/file/downloadZip.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ folder: folder, files: window.filesToDownload })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error("Failed to create zip file: " + text);
|
||||
});
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
if (!blob || blob.size === 0) {
|
||||
throw new Error("Received empty zip file.");
|
||||
}
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = url;
|
||||
a.download = zipName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
// Hide the progress modal after download starts
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
showToast("Download started.");
|
||||
})
|
||||
.catch(error => {
|
||||
// Hide the progress modal on error
|
||||
document.getElementById("downloadProgressModal").style.display = "none";
|
||||
console.error("Error downloading zip:", error);
|
||||
showToast("Error downloading selected files as zip: " + error.message);
|
||||
|
||||
// b) Hide the name‐input modal, show the spinner modal
|
||||
zipNameModal.style.display = "none";
|
||||
progressModal.style.display = "block";
|
||||
|
||||
// c) (Optional) update the “Preparing…” text if you gave it an ID
|
||||
const titleEl = document.getElementById("downloadProgressTitle");
|
||||
if (titleEl) titleEl.textContent = `Preparing ${zipName}…`;
|
||||
|
||||
try {
|
||||
// d) POST and await the ZIP blob
|
||||
const res = await fetch("/api/file/downloadZip.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: window.currentFolder || "root",
|
||||
files: window.filesToDownload
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
throw new Error(txt || `Status ${res.status}`);
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
if (!blob || blob.size === 0) {
|
||||
throw new Error("Received empty ZIP file.");
|
||||
}
|
||||
|
||||
// e) Hand off to the browser’s download manager
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = zipName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error downloading ZIP:", err);
|
||||
showToast("Error: " + err.message);
|
||||
} finally {
|
||||
// f) Always hide spinner modal
|
||||
progressModal.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -573,4 +555,22 @@ export function initFileActions() {
|
||||
}
|
||||
}
|
||||
|
||||
// Hook up the single‐file download modal buttons
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const cancelDownloadFileBtn = document.getElementById("cancelDownloadFile");
|
||||
if (cancelDownloadFileBtn) {
|
||||
cancelDownloadFileBtn.addEventListener("click", () => {
|
||||
document.getElementById("downloadFileModal").style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
const confirmSingleDownloadBtn = document.getElementById("confirmSingleDownloadButton");
|
||||
if (confirmSingleDownloadBtn) {
|
||||
confirmSingleDownloadBtn.addEventListener("click", confirmSingleDownload);
|
||||
}
|
||||
|
||||
// Make Enter also confirm the download
|
||||
attachEnterKeyListener("downloadFileModal", "confirmSingleDownloadButton");
|
||||
});
|
||||
|
||||
window.renameFile = renameFile;
|
||||
@@ -340,6 +340,48 @@ export function renderFileTable(folder, container) {
|
||||
|
||||
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||
|
||||
// 1) Row-click selects the row
|
||||
fileListContent.querySelectorAll("tbody tr").forEach(row => {
|
||||
row.addEventListener("click", e => {
|
||||
// grab the underlying checkbox value
|
||||
const cb = row.querySelector(".file-checkbox");
|
||||
if (!cb) return;
|
||||
toggleRowSelection(e, cb.value);
|
||||
});
|
||||
});
|
||||
|
||||
// 2) Download buttons
|
||||
fileListContent.querySelectorAll(".download-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
openDownloadModal(btn.dataset.downloadName, btn.dataset.downloadFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// 3) Edit buttons
|
||||
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
editFile(btn.dataset.editName, btn.dataset.editFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// 4) Rename buttons
|
||||
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
renameFile(btn.dataset.renameName, btn.dataset.renameFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// 5) Preview buttons (if you still have a .preview-btn)
|
||||
fileListContent.querySelectorAll(".preview-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
previewFile(btn.dataset.previewUrl, btn.dataset.previewName);
|
||||
});
|
||||
});
|
||||
|
||||
createViewToggleButton();
|
||||
|
||||
// Setup event listeners.
|
||||
@@ -476,23 +518,26 @@ export function renderGalleryView(folder, container) {
|
||||
|
||||
pageFiles.forEach((file, idx) => {
|
||||
const idSafe = encodeURIComponent(file.name) + "-" + (startIdx + idx);
|
||||
const cacheKey = folderPath + encodeURIComponent(file.name);
|
||||
|
||||
// thumbnail
|
||||
let thumbnail;
|
||||
if (/\.(jpe?g|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||||
const cacheKey = folderPath + encodeURIComponent(file.name);
|
||||
if (window.imageCache && window.imageCache[cacheKey]) {
|
||||
thumbnail = `<img src="${window.imageCache[cacheKey]}"
|
||||
class="gallery-thumbnail"
|
||||
alt="${escapeHTML(file.name)}"
|
||||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||
thumbnail = `<img
|
||||
src="${window.imageCache[cacheKey]}"
|
||||
class="gallery-thumbnail"
|
||||
data-cache-key="${cacheKey}"
|
||||
alt="${escapeHTML(file.name)}"
|
||||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||
} else {
|
||||
const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + Date.now();
|
||||
thumbnail = `<img src="${imageUrl}"
|
||||
onload="cacheImage(this,'${cacheKey}')"
|
||||
class="gallery-thumbnail"
|
||||
alt="${escapeHTML(file.name)}"
|
||||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||
thumbnail = `<img
|
||||
src="${imageUrl}"
|
||||
class="gallery-thumbnail"
|
||||
data-cache-key="${cacheKey}"
|
||||
alt="${escapeHTML(file.name)}"
|
||||
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||
}
|
||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
|
||||
@@ -529,9 +574,9 @@ export function renderGalleryView(folder, container) {
|
||||
<label for="cb-${idSafe}"
|
||||
style="position:absolute; top:5px; left:5px; width:16px; height:16px;"></label>
|
||||
|
||||
<div class="gallery-preview"
|
||||
style="cursor:pointer;"
|
||||
onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t='+Date.now(), '${file.name}')">
|
||||
<div class="gallery-preview" style="cursor:pointer;"
|
||||
data-preview-url="${folderPath+encodeURIComponent(file.name)}?t=${Date.now()}"
|
||||
data-preview-name="${file.name}">
|
||||
${thumbnail}
|
||||
</div>
|
||||
|
||||
@@ -544,22 +589,25 @@ export function renderGalleryView(folder, container) {
|
||||
|
||||
<div class="button-wrap" style="display:flex; justify-content:center; gap:5px; margin-top:5px;">
|
||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
||||
onclick="openDownloadModal('${file.name}', '${file.folder || "root"}')"
|
||||
data-download-name="${escapeHTML(file.name)}"
|
||||
data-download-folder="${file.folder||"root"}"
|
||||
title="${t('download')}">
|
||||
<i class="material-icons">file_download</i>
|
||||
</button>
|
||||
${file.editable ? `
|
||||
<button class="btn btn-sm edit-btn"
|
||||
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
title="${t('Edit')}">
|
||||
<button type="button" class="btn btn-sm edit-btn"
|
||||
data-edit-name="${escapeHTML(file.name)}"
|
||||
data-edit-folder="${file.folder||"root"}"
|
||||
title="${t('edit')}">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>` : ""}
|
||||
<button class="btn btn-sm btn-warning rename-btn"
|
||||
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
<button type="button" class="btn btn-sm btn-warning rename-btn"
|
||||
data-rename-name="${escapeHTML(file.name)}"
|
||||
data-rename-folder="${file.folder||"root"}"
|
||||
title="${t('rename')}">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary share-btn"
|
||||
<button type="button" class="btn btn-sm btn-secondary share-btn"
|
||||
data-file="${escapeHTML(file.name)}"
|
||||
title="${t('share')}">
|
||||
<i class="material-icons">share</i>
|
||||
@@ -579,13 +627,59 @@ export function renderGalleryView(folder, container) {
|
||||
// render
|
||||
fileListContent.innerHTML = galleryHTML;
|
||||
|
||||
// ensure toggle button
|
||||
createViewToggleButton();
|
||||
// --- Now wire up all behaviors without inline handlers ---
|
||||
|
||||
// attach listeners
|
||||
// cache images on load
|
||||
fileListContent.querySelectorAll('.gallery-thumbnail').forEach(img => {
|
||||
const key = img.dataset.cacheKey;
|
||||
img.addEventListener('load', () => cacheImage(img, key));
|
||||
});
|
||||
|
||||
// preview clicks
|
||||
fileListContent.querySelectorAll(".gallery-preview").forEach(el => {
|
||||
el.addEventListener("click", () => {
|
||||
previewFile(el.dataset.previewUrl, el.dataset.previewName);
|
||||
});
|
||||
});
|
||||
|
||||
// download clicks
|
||||
fileListContent.querySelectorAll(".download-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
openDownloadModal(btn.dataset.downloadName, btn.dataset.downloadFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// edit clicks
|
||||
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
editFile(btn.dataset.editName, btn.dataset.editFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// rename clicks
|
||||
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
renameFile(btn.dataset.renameName, btn.dataset.renameFolder);
|
||||
});
|
||||
});
|
||||
|
||||
// share clicks
|
||||
fileListContent.querySelectorAll(".share-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
const fileName = btn.dataset.file;
|
||||
const fileObj = fileData.find(f => f.name === fileName);
|
||||
if (fileObj) {
|
||||
import('./filePreview.js').then(m => m.openShareModal(fileObj, folder));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// checkboxes
|
||||
document.querySelectorAll(".file-checkbox").forEach(cb => {
|
||||
fileListContent.querySelectorAll(".file-checkbox").forEach(cb => {
|
||||
cb.addEventListener("change", () => updateFileActionButtons());
|
||||
});
|
||||
|
||||
@@ -603,14 +697,13 @@ export function renderGalleryView(folder, container) {
|
||||
});
|
||||
}
|
||||
|
||||
// pagination
|
||||
// pagination functions
|
||||
window.changePage = newPage => {
|
||||
window.currentPage = newPage;
|
||||
if (window.viewMode === "gallery") renderGalleryView(folder);
|
||||
else renderFileTable(folder);
|
||||
};
|
||||
|
||||
// items per page
|
||||
window.changeItemsPerPage = cnt => {
|
||||
window.itemsPerPage = +cnt;
|
||||
localStorage.setItem("itemsPerPage", cnt);
|
||||
@@ -619,8 +712,9 @@ export function renderGalleryView(folder, container) {
|
||||
else renderFileTable(folder);
|
||||
};
|
||||
|
||||
// update toolbar buttons
|
||||
// update toolbar and toggle button
|
||||
updateFileActionButtons();
|
||||
createViewToggleButton();
|
||||
}
|
||||
|
||||
// Responsive slider constraints based on screen size.
|
||||
|
||||
@@ -176,6 +176,8 @@ const translations = {
|
||||
// Dark Mode Toggle
|
||||
"dark_mode_toggle": "Dark Mode",
|
||||
"light_mode_toggle": "Light Mode",
|
||||
"switch_to_light_mode": "Switch to light mode",
|
||||
"switch_to_dark_mode": "Switch to dark mode",
|
||||
|
||||
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
|
||||
"admin_panel": "Admin Panel",
|
||||
|
||||
@@ -48,6 +48,27 @@ export function loadCsrfToken() {
|
||||
});
|
||||
}
|
||||
|
||||
// 1) Immediately clear “?logout=1” flag
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('logout') === '1') {
|
||||
localStorage.removeItem("username");
|
||||
localStorage.removeItem("userTOTPEnabled");
|
||||
}
|
||||
|
||||
// 2) Wire up logoutBtn right away
|
||||
const logoutBtn = document.getElementById("logoutBtn");
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener("click", () => {
|
||||
fetch("/api/auth/logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
})
|
||||
.then(() => window.location.reload(true))
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Expose functions for inline handlers.
|
||||
window.sendRequest = sendRequest;
|
||||
@@ -118,48 +139,55 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
// --- Dark Mode Persistence ---
|
||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||
const storedDarkMode = localStorage.getItem("darkMode");
|
||||
const darkModeIcon = document.getElementById("darkModeIcon");
|
||||
|
||||
if (storedDarkMode === "true") {
|
||||
document.body.classList.add("dark-mode");
|
||||
} else if (storedDarkMode === "false") {
|
||||
document.body.classList.remove("dark-mode");
|
||||
} else {
|
||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
document.body.classList.add("dark-mode");
|
||||
} else {
|
||||
document.body.classList.remove("dark-mode");
|
||||
if (darkModeToggle && darkModeIcon) {
|
||||
// 1) Load stored preference (or null)
|
||||
let stored = localStorage.getItem("darkMode");
|
||||
const hasStored = stored !== null;
|
||||
|
||||
// 2) Determine initial mode
|
||||
const isDark = hasStored
|
||||
? (stored === "true")
|
||||
: (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
|
||||
document.body.classList.toggle("dark-mode", isDark);
|
||||
darkModeToggle.classList.toggle("active", isDark);
|
||||
|
||||
// 3) Helper to update icon & aria-label
|
||||
function updateIcon() {
|
||||
const dark = document.body.classList.contains("dark-mode");
|
||||
darkModeIcon.textContent = dark ? "light_mode" : "dark_mode";
|
||||
darkModeToggle.setAttribute(
|
||||
"aria-label",
|
||||
dark ? t("light_mode") : t("dark_mode")
|
||||
);
|
||||
darkModeToggle.setAttribute(
|
||||
"title",
|
||||
dark
|
||||
? t("switch_to_light_mode")
|
||||
: t("switch_to_dark_mode")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (darkModeToggle) {
|
||||
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
|
||||
? t("light_mode")
|
||||
: t("dark_mode");
|
||||
updateIcon();
|
||||
|
||||
darkModeToggle.addEventListener("click", function () {
|
||||
if (document.body.classList.contains("dark-mode")) {
|
||||
document.body.classList.remove("dark-mode");
|
||||
localStorage.setItem("darkMode", "false");
|
||||
darkModeToggle.textContent = t("dark_mode");
|
||||
} else {
|
||||
document.body.classList.add("dark-mode");
|
||||
localStorage.setItem("darkMode", "true");
|
||||
darkModeToggle.textContent = t("light_mode");
|
||||
}
|
||||
// 4) Click handler: always override and store preference
|
||||
darkModeToggle.addEventListener("click", () => {
|
||||
const nowDark = document.body.classList.toggle("dark-mode");
|
||||
localStorage.setItem("darkMode", nowDark ? "true" : "false");
|
||||
updateIcon();
|
||||
});
|
||||
}
|
||||
|
||||
if (localStorage.getItem("darkMode") === null && window.matchMedia) {
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => {
|
||||
if (event.matches) {
|
||||
document.body.classList.add("dark-mode");
|
||||
if (darkModeToggle) darkModeToggle.textContent = t("light_mode");
|
||||
} else {
|
||||
document.body.classList.remove("dark-mode");
|
||||
if (darkModeToggle) darkModeToggle.textContent = t("dark_mode");
|
||||
}
|
||||
});
|
||||
// 5) OS‐level change: only if no stored pref at load
|
||||
if (!hasStored && window.matchMedia) {
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", e => {
|
||||
document.body.classList.toggle("dark-mode", e.matches);
|
||||
updateIcon();
|
||||
});
|
||||
}
|
||||
}
|
||||
// --- End Dark Mode Persistence ---
|
||||
|
||||
|
||||
@@ -402,19 +402,19 @@ class FolderController
|
||||
* @return void Outputs HTML content.
|
||||
*/
|
||||
|
||||
function formatBytes($bytes)
|
||||
{
|
||||
if ($bytes < 1024) {
|
||||
return $bytes . " B";
|
||||
} elseif ($bytes < 1024 * 1024) {
|
||||
return round($bytes / 1024, 2) . " KB";
|
||||
} elseif ($bytes < 1024 * 1024 * 1024) {
|
||||
return round($bytes / (1024 * 1024), 2) . " MB";
|
||||
} else {
|
||||
return round($bytes / (1024 * 1024 * 1024), 2) . " GB";
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes($bytes)
|
||||
{
|
||||
if ($bytes < 1024) {
|
||||
return $bytes . " B";
|
||||
} elseif ($bytes < 1024 * 1024) {
|
||||
return round($bytes / 1024, 2) . " KB";
|
||||
} elseif ($bytes < 1024 * 1024 * 1024) {
|
||||
return round($bytes / (1024 * 1024), 2) . " MB";
|
||||
} else {
|
||||
return round($bytes / (1024 * 1024 * 1024), 2) . " GB";
|
||||
}
|
||||
}
|
||||
|
||||
public function shareFolder(): void
|
||||
{
|
||||
// Retrieve GET parameters.
|
||||
@@ -759,72 +759,83 @@ class FolderController
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// (Optional) JavaScript for toggling view modes (list/gallery).
|
||||
var viewMode = 'list';
|
||||
window.imageCache = window.imageCache || {};
|
||||
var filesData = <?php echo json_encode($files); ?>;
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// JavaScript for toggling view modes (list/gallery) and wiring up gallery clicks
|
||||
var viewMode = 'list';
|
||||
var token = '<?php echo addslashes($token); ?>';
|
||||
var filesData = <?php echo json_encode($files); ?>;
|
||||
|
||||
// Use the shared‑folder relative path (from your model), not realFolderPath
|
||||
// $data['folder'] should be something like "eafwef/testfolder2/test/new folder two"
|
||||
var rawRelPath = "<?php echo addslashes($data['folder']); ?>";
|
||||
// Split into segments, encode each segment, then re-join
|
||||
var folderSegments = rawRelPath
|
||||
.split('/')
|
||||
.map(encodeURIComponent)
|
||||
.join('/');
|
||||
// Build the download URL base
|
||||
var downloadBase = window.location.origin +
|
||||
'/api/folder/downloadSharedFile.php?token=' +
|
||||
encodeURIComponent(token) +
|
||||
'&file=';
|
||||
|
||||
function renderGalleryView() {
|
||||
var galleryContainer = document.getElementById("galleryViewContainer");
|
||||
var html = '<div class="shared-gallery-container">';
|
||||
filesData.forEach(function(file) {
|
||||
// Encode the filename too
|
||||
var fileName = encodeURIComponent(file);
|
||||
var fileUrl = window.location.origin +
|
||||
'/uploads/' +
|
||||
folderSegments +
|
||||
'/' +
|
||||
fileName +
|
||||
'?t=' +
|
||||
Date.now();
|
||||
function toggleViewMode() {
|
||||
var listEl = document.getElementById('listViewContainer');
|
||||
var galleryEl = document.getElementById('galleryViewContainer');
|
||||
var btn = document.getElementById('toggleBtn');
|
||||
|
||||
var ext = file.split('.').pop().toLowerCase();
|
||||
var thumbnail;
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].indexOf(ext) >= 0) {
|
||||
thumbnail = '<img src="' + fileUrl + '" alt="' + file + '">';
|
||||
if (viewMode === 'list') {
|
||||
viewMode = 'gallery';
|
||||
listEl.style.display = 'none';
|
||||
renderGalleryView();
|
||||
galleryEl.style.display = 'block';
|
||||
btn.textContent = 'Switch to List View';
|
||||
} else {
|
||||
thumbnail = '<span class="material-icons">insert_drive_file</span>';
|
||||
viewMode = 'list';
|
||||
galleryEl.style.display = 'none';
|
||||
listEl.style.display = 'block';
|
||||
btn.textContent = 'Switch to Gallery View';
|
||||
}
|
||||
|
||||
html +=
|
||||
'<div class="shared-gallery-card">' +
|
||||
'<div class="gallery-preview" ' +
|
||||
'onclick="window.location.href=\'' + fileUrl + '\'" ' +
|
||||
'style="cursor:pointer;">' +
|
||||
thumbnail +
|
||||
'</div>' +
|
||||
'<div class="gallery-info">' +
|
||||
'<span class="gallery-file-name">' + file + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
galleryContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
function toggleViewMode() {
|
||||
if (viewMode === 'list') {
|
||||
viewMode = 'gallery';
|
||||
document.getElementById("listViewContainer").style.display = "none";
|
||||
renderGalleryView();
|
||||
document.getElementById("galleryViewContainer").style.display = "block";
|
||||
document.getElementById("toggleBtn").textContent = "Switch to List View";
|
||||
} else {
|
||||
viewMode = 'list';
|
||||
document.getElementById("galleryViewContainer").style.display = "none";
|
||||
document.getElementById("listViewContainer").style.display = "block";
|
||||
document.getElementById("toggleBtn").textContent = "Switch to Gallery View";
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up the toggle button
|
||||
document.getElementById('toggleBtn')
|
||||
.addEventListener('click', toggleViewMode);
|
||||
|
||||
function renderGalleryView() {
|
||||
var galleryContainer = document.getElementById('galleryViewContainer');
|
||||
var html = '<div class="shared-gallery-container">';
|
||||
|
||||
filesData.forEach(function(file) {
|
||||
var encodedName = encodeURIComponent(file);
|
||||
var fileUrl = downloadBase + encodedName;
|
||||
var ext = file.split('.').pop().toLowerCase();
|
||||
var thumb;
|
||||
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].indexOf(ext) >= 0) {
|
||||
thumb = '<img src="' + fileUrl + '" alt="' + file + '">';
|
||||
} else {
|
||||
thumb = '<span class="material-icons">insert_drive_file</span>';
|
||||
}
|
||||
|
||||
html +=
|
||||
'<div class="shared-gallery-card">' +
|
||||
'<div class="gallery-preview" data-url="' + fileUrl + '" style="cursor:pointer;">' +
|
||||
thumb +
|
||||
'</div>' +
|
||||
'<div class="gallery-info">' +
|
||||
'<span class="gallery-file-name">' + file + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
galleryContainer.innerHTML = html;
|
||||
|
||||
// Wire up each thumbnail click
|
||||
galleryContainer.querySelectorAll('.gallery-preview')
|
||||
.forEach(function(el) {
|
||||
el.addEventListener('click', function() {
|
||||
window.location.href = el.dataset.url;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Expose for manual invocation if needed
|
||||
window.renderGalleryView = renderGalleryView;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user