release(v1.7.5): CSP hardening, API-backed previews, flicker-free theming, cache tuning & deploy script (closes #50)

This commit is contained in:
Ryan
2025-11-02 00:32:03 -04:00
committed by GitHub
parent e509b7ac9c
commit b7d7f7c3ce
11 changed files with 699 additions and 336 deletions

View File

@@ -1,5 +1,43 @@
# Changelog # Changelog
## Changes 11/2/2025 (v1.7.5)
release(v1.7.5): CSP hardening, API-backed previews, flicker-free theming, cache tuning & deploy script (closes #50)
### Security/headers
- Tighten CSP: pin the inline pre-theme snippet with a script-src SHA-256 and keep everything else on 'self'.
- Improve cache policy for versioned assets: force 1y + immutable and add s-maxage for CDNs; also avoid HSTS redirects on local/dev hosts.
### Previews & editor
- Remove hardcoded `/uploads/` paths; always build preview URLs via the API (respects UPLOAD_DIR/ACL).
- Use the API URL for gallery prev/next and file-menu “Preview” to fix 404s on custom storage roots.
- Editor now probes size safely (HEAD → Range 0-0 fallback) before fetching, then fetches with credentials.
### Login, theming & UX polish
- Pre-theme inline boot sets `dark-mode` + background early; swap to `[hidden]`/`unhide()` instead of inline `display:none`.
- Add full-screen loading overlay with quick fade and proper color-scheme; prevent white/black flash on theme flips.
- Refactor app/login reveal flow in `main.js` (`revealAppAndHideOverlay`, `authed` path, setup wizard).
### HTML/CSS & perf
- Make Bootstrap/Styles/Roboto critical (plain `<link rel="stylesheet">`); keep fonts as true preloads; modulepreload app entry.
- Export a `__CSS_PROMISE__` from `defer-css.js` for sites that still promote preloads.
- Header logo marked `fetchpriority="high"` for faster first paint.
- Normalize dark-mode selectors to `.dark-mode` scope (admin panel, etc.).
### Manual Deploy script
- Add `scripts/filerise-deploy.sh`: idempotent rsync-based deploy with writable dirs preserved, optional Composer install, and PHP-FPM/Apache reloads.
### Notes
- If you change the inline pre-theme snippet, update the CSP hash accordingly.
---
## Changes 10/31/2025 (v1.7.4) ## Changes 10/31/2025 (v1.7.4)
release(v1.7.4): login hint replace toast + fix unauth boot release(v1.7.4): login hint replace toast + fix unauth boot

View File

@@ -11,6 +11,9 @@ DirectoryIndex index.html
</IfModule> </IfModule>
RewriteEngine On RewriteEngine On
# Never redirect local/dev hosts
RewriteCond %{HTTP_HOST} ^(localhost|127\.0\.0\.1|fr\.local|192\.168\.[0-9]+\.[0-9]+)$ [NC]
RewriteRule ^ - [L]
# --- HTTPS redirect --- # --- HTTPS redirect ---
# Use ONE of these blocks. # Use ONE of these blocks.
@@ -52,7 +55,7 @@ RewriteEngine On
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" "expr=%{HTTPS} == 'on'" Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" "expr=%{HTTPS} == 'on'"
# CSP (modules, blobs, workers, etc.) # CSP (modules, blobs, workers, etc.)
Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; worker-src 'self' blob:; form-action 'self'" Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; script-src 'self' 'sha256-ajmGY+5VJOY6+8JHgzCqsqI8w9dCQfAmqIkFesOKItM='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; worker-src 'self' blob:; form-action 'self'"
</IfModule> </IfModule>
# --- Caching (query-string based, no env vars needed) --- # --- Caching (query-string based, no env vars needed) ---
@@ -81,10 +84,15 @@ RewriteEngine On
Header set Cache-Control "public, max-age=604800" "expr=%{QUERY_STRING} !~ /(^|&)v=/" Header set Cache-Control "public, max-age=604800" "expr=%{QUERY_STRING} !~ /(^|&)v=/"
</FilesMatch> </FilesMatch>
# Versioned assets (?v=...): 1 year + immutable # --- Versioned assets (?v=...) : 1 year + immutable (override anything else) ---
<FilesMatch "\.(?:m?js|css|png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$"> <IfModule mod_headers.c>
Header setifempty Cache-Control "public, max-age=31536000, immutable" "expr=%{QUERY_STRING} =~ /(^|&)v=/" <FilesMatch "\.(?:m?js|css|png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$">
</FilesMatch> # Only when query string has v=
Header unset Cache-Control "expr=%{QUERY_STRING} =~ /(^|&)v=/"
Header unset Expires "expr=%{QUERY_STRING} =~ /(^|&)v=/"
Header set Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable" "expr=%{QUERY_STRING} =~ /(^|&)v=/"
</FilesMatch>
</IfModule>
</IfModule> </IfModule>
# --- Compression --- # --- Compression ---

View File

@@ -1,6 +1,38 @@
/* =========================================================== /* ===========================================================
GENERAL STYLES & BASE LAYOUT GENERAL STYLES & BASE LAYOUT
=========================================================== */ =========================================================== */
/* Reserve stable space for header + main */
:root { --header-h: 55px; }
.header-container { min-height: var(--header-h); }
img.logo{ width:50px; height:50px; display:block; } /* belt & suspenders for logo sizing */
/* Hidden-but-reserved utility (no clicks) */
.is-visually-hidden {
visibility: hidden;
pointer-events: none;
}
/* After auth: show app, hide login */
#fr-login-tip {
min-height: 40px; /* reserve space */
max-width: 520px;
margin: 8px auto 0;
border-radius: 8px;
padding: 10px 12px;
text-align: left;
margin-bottom: 10px;
}
.main-wrapper{
display:flex; /* or grid—flex is fine here */
gap:5px;
align-items:flex-start;
}
/* GENERAL STYLES */ /* GENERAL STYLES */
body { body {
@@ -24,8 +56,8 @@ body {
padding-left: 4px !important; padding-left: 4px !important;
}@media (min-width: 1300px) { }@media (min-width: 1300px) {
.container-fluid { .container-fluid {
padding-left: 30px !important; padding-left: 20px !important;
padding-right: 30px !important; padding-right: 20px !important;
}} }}
@media (max-width: 600px) { @media (max-width: 600px) {
.zones-toggle { left: 85px !important; } .zones-toggle { left: 85px !important; }
@@ -37,11 +69,6 @@ body {
/************************************************************/ /************************************************************/
/* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */ /* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */
/************************************************************/ /************************************************************/
.header-logo .logo {
display:block;
max-width:100%;
height:auto; /* keep aspect ratio; HTML attrs set the intrinsic box */
}
.btn-login { .btn-login {
margin-top: 10px; margin-top: 10px;
}/* Color overrides */ }/* Color overrides */
@@ -65,7 +92,7 @@ body {
background-color: #2196F3; background-color: #2196F3;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}body.dark-mode .header-container { }.dark-mode .header-container {
background-color: #1f1f1f; background-color: #1f1f1f;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
}#darkModeIcon { }#darkModeIcon {
@@ -77,7 +104,7 @@ body {
}.header-logo svg { }.header-logo svg {
height: 50px; height: 50px;
width: auto; width: auto;
}body.dark-mode header { }.dark-mode header {
background-color: #1f1f1f; background-color: #1f1f1f;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
}.header-left { }.header-left {
@@ -163,7 +190,7 @@ body {
padding: 10px; padding: 10px;
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2); box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2);
}/* Folder Help Tooltip - Dark Mode */ }/* Folder Help Tooltip - Dark Mode */
body.dark-mode .folder-help-tooltip { .dark-mode .folder-help-tooltip {
background-color: #333 !important; background-color: #333 !important;
color: #eee !important; color: #eee !important;
border: 1px solid #555 !important; border: 1px solid #555 !important;
@@ -171,7 +198,7 @@ body {
-webkit-text-fill-color: orange !important; -webkit-text-fill-color: orange !important;
color: inherit !important; color: inherit !important;
padding-right: 10px !important; padding-right: 10px !important;
}body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon { }.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
-webkit-text-fill-color: #ffa500 !important; -webkit-text-fill-color: #ffa500 !important;
padding-right: 10px !important; padding-right: 10px !important;
}/************************************************************/ }/************************************************************/
@@ -221,8 +248,8 @@ body {
.material-icons.gallery-icon { .material-icons.gallery-icon {
color: black; color: black;
margin-right: 5px; margin-right: 5px;
}body.dark-mode .material-icons.folder-icon, }.dark-mode .material-icons.folder-icon,
body.dark-mode .material-icons.gallery-icon { .dark-mode .material-icons.gallery-icon {
color: white; color: white;
margin-right: 5px; margin-right: 5px;
}.remove-file-btn { }.remove-file-btn {
@@ -253,23 +280,23 @@ body {
padding: 20px; padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 4px; border-radius: 4px;
}body.dark-mode #loginForm { }.dark-mode #loginForm {
background-color: #2c2c2c; background-color: #2c2c2c;
color: #e0e0e0; color: #e0e0e0;
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 4px rgba(255, 255, 255, 0.2); box-shadow: 0 2px 4px rgba(255, 255, 255, 0.2);
}body.dark-mode #loginForm input { }.dark-mode #loginForm input {
background-color: #333; background-color: #333;
color: #fff; color: #fff;
border: 1px solid #555; border: 1px solid #555;
}body.dark-mode #loginForm label { }.dark-mode #loginForm label {
color: #ddd; color: #ddd;
}body.dark-mode #loginForm button { }.dark-mode #loginForm button {
background-color: #007bff; background-color: #007bff;
color: white; color: white;
border: none; border: none;
}body.dark-mode #loginForm button:hover { }.dark-mode #loginForm button:hover {
background-color: #0056b3; background-color: #0056b3;
}/* =========================================================== }/* ===========================================================
CARDS & MODALS CARDS & MODALS
@@ -292,7 +319,7 @@ body {
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px; border-radius: 4px;
}/* Override modal content for dark mode */ }/* Override modal content for dark mode */
body.dark-mode #restoreFilesModal .modal-content { .dark-mode #restoreFilesModal .modal-content {
background: #2c2c2c !important; background: #2c2c2c !important;
border: 1px solid #555 !important; border: 1px solid #555 !important;
color: #f0f0f0; color: #f0f0f0;
@@ -376,7 +403,7 @@ body {
transform: translate(-50%, -70%); transform: translate(-50%, -70%);
}} }}
body.dark-mode .modal .modal-content { .dark-mode .modal .modal-content {
background-color: #2c2c2c; background-color: #2c2c2c;
color: #e0e0e0; color: #e0e0e0;
border-color: #444; border-color: #444;
@@ -405,10 +432,10 @@ body {
background-color: #ff4d4d; background-color: #ff4d4d;
box-shadow: 0px 0px 6px rgba(255, 77, 77, 0.8); box-shadow: 0px 0px 6px rgba(255, 77, 77, 0.8);
transform: scale(1.05); transform: scale(1.05);
}body.dark-mode .editor-close-btn { }.dark-mode .editor-close-btn {
background-color: rgba(0, 0, 0, 0.7); background-color: rgba(0, 0, 0, 0.7);
color: #ff6666; color: #ff6666;
}body.dark-mode .editor-close-btn:hover { }.dark-mode .editor-close-btn:hover {
background-color: #ff6666; background-color: #ff6666;
color: #000; color: #000;
}/* Editor Modal */ }/* Editor Modal */
@@ -434,7 +461,7 @@ body {
width: 100% !important; width: 100% !important;
resize: none !important; resize: none !important;
overflow: auto !important; overflow: auto !important;
}body.dark-mode .editor-modal { }.dark-mode .editor-modal {
background-color: #2c2c2c; background-color: #2c2c2c;
color: #e0e0e0; color: #e0e0e0;
border-color: #444; border-color: #444;
@@ -459,7 +486,7 @@ body {
}.editor-title { }.editor-title {
margin: 0; margin: 0;
line-height: 33px; line-height: 33px;
}body.dark-mode .editor-header { }.dark-mode .editor-header {
background-color: #2c2c2c; background-color: #2c2c2c;
}@media (max-width: 600px) { }@media (max-width: 600px) {
.editor-title { .editor-title {
@@ -527,9 +554,9 @@ body {
padding: 4px; padding: 4px;
border-radius: 4px; border-radius: 4px;
transition: background-color 0.2s ease, color 0.2s ease; transition: background-color 0.2s ease, color 0.2s ease;
}body.dark-mode .material-icons.pauseResumeBtn { }.dark-mode .material-icons.pauseResumeBtn {
color: white !important; color: white !important;
}body.dark-mode .material-icons.pauseResumeBtn:hover { }.dark-mode .material-icons.pauseResumeBtn:hover {
background-color: rgba(255, 215, 0, 0.3); background-color: rgba(255, 215, 0, 0.3);
color: #fff; color: #fff;
}body:not(.dark-mode) .material-icons.pauseResumeBtn:hover { }body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
@@ -632,15 +659,15 @@ body {
}#createBtn { }#createBtn {
background-color: #007bff; background-color: #007bff;
color: white; color: white;
}body.dark-mode .dropdown-menu { }.dark-mode .dropdown-menu {
background-color: #2c2c2c !important; background-color: #2c2c2c !important;
border-color: #444 !important; border-color: #444 !important;
color: #e0e0e0!important; color: #e0e0e0!important;
}body.dark-mode .dropdown-menu .dropdown-item { }.dark-mode .dropdown-menu .dropdown-item {
color: #e0e0e0 !important; color: #e0e0e0 !important;
}.dropdown-item:hover { }.dropdown-item:hover {
background-color: rgba(0,0,0,0.05); background-color: rgba(0,0,0,0.05);
}body.dark-mode .dropdown-item:hover { }.dark-mode .dropdown-item:hover {
background-color: rgba(255,255,255,0.1); background-color: rgba(255,255,255,0.1);
}#fileList button.edit-btn { }#fileList button.edit-btn {
background-color: #007bff; background-color: #007bff;
@@ -661,7 +688,7 @@ body {
background-color: transparent; background-color: transparent;
}#fileList table tr:hover { }#fileList table tr:hover {
background-color: #e0e0e0; background-color: #e0e0e0;
}body.dark-mode #fileList table tr:hover { }.dark-mode #fileList table tr:hover {
background-color: #444; background-color: #444;
}#fileListTitle { }#fileListTitle {
white-space: normal !important; white-space: normal !important;
@@ -679,7 +706,7 @@ body {
box-shadow: none; box-shadow: none;
border: none !important; border: none !important;
outline: none !important; outline: none !important;
}body.dark-mode #fileList table tr { }.dark-mode #fileList table tr {
box-shadow: none; box-shadow: none;
border: none !important; border: none !important;
outline: none !important; outline: none !important;
@@ -763,7 +790,7 @@ body {
color: inherit; color: inherit;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
}#loginForm, }
#uploadForm { #uploadForm {
display: none; display: none;
}.folder-actions { }.folder-actions {
@@ -824,7 +851,7 @@ body {
color: #fff; color: #fff;
}.row-selected { }.row-selected {
background-color: #f2f2f2 !important; background-color: #f2f2f2 !important;
}body.dark-mode .row-selected { }.dark-mode .row-selected {
background-color: #444 !important; background-color: #444 !important;
color: #fff !important; color: #fff !important;
}.custom-prev-next-btn { }.custom-prev-next-btn {
@@ -838,11 +865,11 @@ body {
cursor: pointer; cursor: pointer;
}.custom-prev-next-btn:hover:not(:disabled) { }.custom-prev-next-btn:hover:not(:disabled) {
background-color: #d5d5d5; background-color: #d5d5d5;
}body.dark-mode .custom-prev-next-btn { }.dark-mode .custom-prev-next-btn {
background-color: #444; background-color: #444;
color: #fff; color: #fff;
border: none; border: none;
}body.dark-mode .custom-prev-next-btn:hover:not(:disabled) { }.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
background-color: #555; background-color: #555;
}#customToast { }#customToast {
position: fixed; position: fixed;
@@ -879,6 +906,10 @@ body {
line-height: 1 !important; line-height: 1 !important;
vertical-align: middle !important; vertical-align: middle !important;
}#fileListContainer { }#fileListContainer {
border: 1px solid #e0e0e0;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 8px;
max-width: 100%; max-width: 100%;
padding-bottom: 10px !important; padding-bottom: 10px !important;
padding-left: 5px !important; padding-left: 5px !important;
@@ -889,7 +920,7 @@ body {
width: 99%; width: 99%;
}} }}
body.dark-mode #fileListContainer { .dark-mode #fileListContainer {
background-color: #2c2c2c; background-color: #2c2c2c;
color: #e0e0e0; color: #e0e0e0;
border: 1px solid #444; border: 1px solid #444;
@@ -1122,12 +1153,12 @@ body {
background-color: #d0d0d0; background-color: #d0d0d0;
border-radius: 4px; border-radius: 4px;
padding: 2px 4px; padding: 2px 4px;
}body.dark-mode .folder-option.selected { }.dark-mode .folder-option.selected {
background-color: #444; background-color: #444;
color: #fff; color: #fff;
border-radius: 4px; border-radius: 4px;
padding: 2px 4px; padding: 2px 4px;
}body.dark-mode .folder-option:hover { }.dark-mode .folder-option:hover {
background-color: #333; background-color: #333;
color: #fff; color: #fff;
padding: 2px 4px; padding: 2px 4px;
@@ -1167,7 +1198,7 @@ body {
display: inline-flex !important; display: inline-flex !important;
}} }}
body.dark-mode .image-preview-modal-content { .dark-mode .image-preview-modal-content {
background: #2c2c2c; background: #2c2c2c;
border-color: #444; border-color: #444;
}.image-modal-img { }.image-modal-img {
@@ -1204,13 +1235,13 @@ body {
width: 600px !important; width: 600px !important;
max-width: 90vw !important; max-width: 90vw !important;
/* ensures it doesn't exceed the viewport width */ /* ensures it doesn't exceed the viewport width */
}body.dark-mode .close-image-modal { }.dark-mode .close-image-modal {
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.6);
color: #ff6666; color: #ff6666;
}body.dark-mode .close-image-modal:hover { }.dark-mode .close-image-modal:hover {
background-color: #ff6666; background-color: #ff6666;
color: #000; color: #000;
}body.dark-mode .image-preview-modal-content { }.dark-mode .image-preview-modal-content {
background: #2c2c2c; background: #2c2c2c;
border-color: #444; border-color: #444;
}.page-indicator { }.page-indicator {
@@ -1223,7 +1254,7 @@ body {
margin-right: 0; margin-right: 0;
margin-left: 0; margin-left: 0;
font-size: 32px; font-size: 32px;
}body.dark-mode .file-icon { }.dark-mode .file-icon {
color: white; color: white;
}.bottom-select { }.bottom-select {
display: inline-block; display: inline-block;
@@ -1321,36 +1352,36 @@ body {
}/* =========================================================== }/* ===========================================================
DARK MODE STYLES DARK MODE STYLES
=========================================================== */ =========================================================== */
body.dark-mode { .dark-mode {
background-color: #121212; background-color: #121212;
color: #e0e0e0; color: #e0e0e0;
}body.dark-mode .container { }.dark-mode .container {
background-color: transparent !important; background-color: transparent !important;
}body.dark-mode .btn-primary { }.dark-mode .btn-primary {
background-color: #007bff; background-color: #007bff;
color: #fff; color: #fff;
border-color: #007bff; border-color: #007bff;
}body.dark-mode .btn-secondary { }.dark-mode .btn-secondary {
background-color: #6c757d; background-color: #6c757d;
color: #fff; color: #fff;
border-color: #6c757d; border-color: #6c757d;
}body.dark-mode .btn-danger { }.dark-mode .btn-danger {
background-color: #dc3545; background-color: #dc3545;
color: #fff; color: #fff;
border-color: #dc3545; border-color: #dc3545;
}body.dark-mode .modal .modal-content, }.dark-mode .modal .modal-content,
body.dark-mode .editor-modal { .dark-mode .editor-modal {
background-color: #2c2c2c; background-color: #2c2c2c;
color: #e0e0e0; color: #e0e0e0;
border: 1px solid #444; border: 1px solid #444;
}body.dark-mode table { }.dark-mode table {
background-color: #2c2c2c; background-color: #2c2c2c;
color: #e0e0e0; color: #e0e0e0;
}body.dark-mode table tr:hover { }.dark-mode table tr:hover {
background-color: #444; background-color: #444;
}body.dark-mode #uploadProgressContainer .progress { }.dark-mode #uploadProgressContainer .progress {
background-color: #333; background-color: #333;
}body.dark-mode #uploadProgressContainer .progress-bar { }.dark-mode #uploadProgressContainer .progress-bar {
background-color: #007bff; background-color: #007bff;
color: #e0e0e0; color: #e0e0e0;
}.dark-mode-toggle { }.dark-mode-toggle {
@@ -1367,10 +1398,10 @@ body {
background-color: rgba(255, 255, 255, 0.15) !important; background-color: rgba(255, 255, 255, 0.15) !important;
}.dark-mode-toggle:active { }.dark-mode-toggle:active {
background-color: rgba(255, 255, 255, 0.25) !important; background-color: rgba(255, 255, 255, 0.25) !important;
}body.dark-mode .dark-mode-toggle { }.dark-mode .dark-mode-toggle {
background-color: transparent !important; background-color: transparent !important;
color: white !important; color: white !important;
}body.dark-mode .dark-mode-toggle:hover { }.dark-mode .dark-mode-toggle:hover {
background-color: rgba(255, 255, 255, 0.15) !important; background-color: rgba(255, 255, 255, 0.15) !important;
}.dark-mode-toggle:focus { }.dark-mode-toggle:focus {
outline: none !important; outline: none !important;
@@ -1397,29 +1428,29 @@ body {
}.folder-help-list { }.folder-help-list {
margin: 0; margin: 0;
padding-left: 20px; padding-left: 20px;
}body.dark-mode .folder-help-details { }.dark-mode .folder-help-details {
color: #ddd; color: #ddd;
background-color: #2c2c2c; background-color: #2c2c2c;
border-color: #444; border-color: #444;
}body.dark-mode .folder-help-summary { }.dark-mode .folder-help-summary {
color: #ddd; color: #ddd;
background: #2c2c2c; background: #2c2c2c;
}body.dark-mode .folder-help-icon { }.dark-mode .folder-help-icon {
color: #f6a72c; color: #f6a72c;
font-size: 20px; font-size: 20px;
}body.dark-mode .CodeMirror { }.dark-mode .CodeMirror {
background: #1e1e1e !important; background: #1e1e1e !important;
color: #ffffff !important; color: #ffffff !important;
}body.dark-mode .CodeMirror-cursor { }.dark-mode .CodeMirror-cursor {
border-left: 2px solid #ffffff !important; border-left: 2px solid #ffffff !important;
}body.dark-mode .CodeMirror-gutters { }.dark-mode .CodeMirror-gutters {
background: #252526 !important; background: #252526 !important;
border-right: 1px solid #444 !important; border-right: 1px solid #444 !important;
}body.dark-mode .CodeMirror-linenumber { }.dark-mode .CodeMirror-linenumber {
color: #aaaaaa !important; color: #aaaaaa !important;
}body.dark-mode .CodeMirror-selected { }.dark-mode .CodeMirror-selected {
background: rgba(255, 255, 255, 0.2) !important; background: rgba(255, 255, 255, 0.2) !important;
}body.dark-mode .CodeMirror-matchingbracket { }.dark-mode .CodeMirror-matchingbracket {
background-color: rgba(255, 255, 255, 0.1) !important; background-color: rgba(255, 255, 255, 0.1) !important;
border-bottom: 1px solid #ffffff !important; border-bottom: 1px solid #ffffff !important;
}.zoom_in, }.zoom_in,
@@ -1454,7 +1485,7 @@ body {
}.drop-hover { }.drop-hover {
background-color: #e0e0e0; background-color: #e0e0e0;
border: 1px dashed #666; border: 1px dashed #666;
}body.dark-mode .drop-hover { }.dark-mode .drop-hover {
background-color: rgba(255, 255, 255, 0.1) !important; background-color: rgba(255, 255, 255, 0.1) !important;
border-bottom: 1px dashed #ffffff !important; border-bottom: 1px dashed #ffffff !important;
}#restoreFilesList li { }#restoreFilesList li {
@@ -1466,35 +1497,33 @@ body {
transform: translateY(-3px) !important; transform: translateY(-3px) !important;
}#restoreFilesList li label { }#restoreFilesList li label {
margin-left: 8px !important; margin-left: 8px !important;
}body.dark-mode #fileContextMenu { }.dark-mode #fileContextMenu {
background-color: #2c2c2c !important; background-color: #2c2c2c !important;
border: 1px solid #555 !important; border: 1px solid #555 !important;
color: #e0e0e0 !important; color: #e0e0e0 !important;
}body.dark-mode #fileContextMenu div { }.dark-mode #fileContextMenu div {
color: #e0e0e0 !important; color: #e0e0e0 !important;
}#folderContextMenu { }#folderContextMenu {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
font-size: 14px; font-size: 14px;
}body.dark-mode #folderContextMenu { }.dark-mode #folderContextMenu {
background-color: #2c2c2c; background-color: #2c2c2c;
border-color: #555; border-color: #555;
color: #e0e0e0; color: #e0e0e0;
}.main-wrapper {
display: flex;
flex-direction: row;
}.drop-target-sidebar { }.drop-target-sidebar {
display: none; display: none;
width: 50px;
transition: width 0.3s ease;
background-color: #f8f9fa; background-color: #f8f9fa;
border-right: 2px dashed #1565C0; border-right: 2px dashed #1565C0;
padding: 10px; margin-top: 10px;
margin-left: 10px;
}@media (min-width: 769px) { }@media (min-width: 769px) {
.drop-target-sidebar { .drop-target-sidebar {
display: block; display: block;
}} }}
.drop-target-sidebar.active { .drop-target-sidebar.active,
.drag-header.active {
width: 350px; width: 350px;
height: 750px;
}.main-column { }.main-column {
flex: 1; flex: 1;
transition: margin-left 0.3s ease; transition: margin-left 0.3s ease;
@@ -1563,13 +1592,12 @@ body {
}#sidebarDropArea, }#sidebarDropArea,
#uploadFolderRow { #uploadFolderRow {
background-color: transparent; background-color: transparent;
}#sidebarDropArea {
display: none; }.dark-mode #sidebarDropArea,
}body.dark-mode #sidebarDropArea, .dark-mode #uploadFolderRow {
body.dark-mode #uploadFolderRow {
background-color: transparent; background-color: transparent;
}body.dark-mode #sidebarDropArea.highlight, }.dark-mode #sidebarDropArea.highlight,
body.dark-mode #uploadFolderRow.highlight { .dark-mode #uploadFolderRow.highlight {
background-color: #333; background-color: #333;
border: 2px dashed #555; border: 2px dashed #555;
color: #fff; color: #fff;
@@ -1588,7 +1616,7 @@ body {
max-width: 900px; max-width: 900px;
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
}body.dark-mode .card { }.dark-mode .card {
background-color: #2c2c2c; background-color: #2c2c2c;
color: #e0e0e0; color: #e0e0e0;
border: 1px solid #444; border: 1px solid #444;
@@ -1606,17 +1634,17 @@ body {
}.admin-panel-content { }.admin-panel-content {
background: #fff; background: #fff;
color: #000; color: #000;
}body.dark-mode .admin-panel-content { }.dark-mode .admin-panel-content {
background: #2c2c2c; background: #2c2c2c;
color: #e0e0e0; color: #e0e0e0;
border: 1px solid #444; border: 1px solid #444;
}body.dark-mode .admin-panel-content input, }.dark-mode .admin-panel-content input,
body.dark-mode .admin-panel-content select, .dark-mode .admin-panel-content select,
body.dark-mode .admin-panel-content textarea { .dark-mode .admin-panel-content textarea {
background: #3a3a3a; background: #3a3a3a;
color: #e0e0e0; color: #e0e0e0;
border: 1px solid #555; border: 1px solid #555;
}body.dark-mode .admin-panel-content label { }.dark-mode .admin-panel-content label {
color: #e0e0e0; color: #e0e0e0;
}#openChangePasswordModalBtn { }#openChangePasswordModalBtn {
width: max-content; width: max-content;
@@ -1637,7 +1665,7 @@ body {
color: var(--download-spinner-color, #000); color: var(--download-spinner-color, #000);
}body:not(.dark-mode) { }body:not(.dark-mode) {
--download-spinner-color: #000; --download-spinner-color: #000;
}body.dark-mode { }.dark-mode {
--download-spinner-color: #fff; --download-spinner-color: #fff;
}.rise-effect { }.rise-effect {
transform: translateY(-20px); transform: translateY(-20px);
@@ -1672,7 +1700,7 @@ body {
background-color: transparent; background-color: transparent;
transition: width 0.3s ease; transition: width 0.3s ease;
box-sizing: border-box; box-sizing: border-box;
}body.dark-mode .header-drop-zone.drag-active { }.dark-mode .header-drop-zone.drag-active {
background-color: #333; background-color: #333;
border: 2px dashed #555; border: 2px dashed #555;
color: #fff; color: #fff;
@@ -1703,16 +1731,16 @@ body {
line-height: 1; line-height: 1;
margin: 0; margin: 0;
padding: 0; padding: 0;
}body.dark-mode #fileSummary { }.dark-mode #fileSummary {
color: white; color: white;
}#searchIcon { }#searchIcon {
border-radius: 4px; border-radius: 4px;
padding: 4px 8px; padding: 4px 8px;
}body.dark-mode #searchIcon { }.dark-mode #searchIcon {
background-color: #444; background-color: #444;
border: 1px solid #555; border: 1px solid #555;
color: #fff; color: #fff;
}body.dark-mode #searchInput { }.dark-mode #searchInput {
background-color: #333; background-color: #333;
color: #e0e0e0; color: #e0e0e0;
border: 1px solid #555; border: 1px solid #555;
@@ -1737,11 +1765,11 @@ body {
.btn-icon:focus { .btn-icon:focus {
background: rgba(0, 0, 0, 0.1); background: rgba(0, 0, 0, 0.1);
outline: none; outline: none;
}body.dark-mode .btn-icon .material-icons, }.dark-mode .btn-icon .material-icons,
body.dark-mode #searchIcon .material-icons { .dark-mode #searchIcon .material-icons {
color: #fff; color: #fff;
}body.dark-mode .btn-icon:hover, }.dark-mode .btn-icon:hover,
body.dark-mode .btn-icon:focus { .dark-mode .btn-icon:focus {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
}.user-dropdown { }.user-dropdown {
position: relative; position: relative;
@@ -1772,12 +1800,12 @@ body {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-left: 0.25rem; margin-left: 0.25rem;
}body.dark-mode .user-dropdown .user-menu { }.dark-mode .user-dropdown .user-menu {
background: #2c2c2c; background: #2c2c2c;
border-color: #444; border-color: #444;
}body.dark-mode .user-dropdown .user-menu .item { }.dark-mode .user-dropdown .user-menu .item {
color: #e0e0e0; color: #e0e0e0;
}body.dark-mode .user-dropdown .user-menu .item:hover { }.dark-mode .user-dropdown .user-menu .item:hover {
background: rgba(255,255,255,0.1); background: rgba(255,255,255,0.1);
}.user-dropdown .dropdown-username { }.user-dropdown .dropdown-username {
margin: 0 8px; margin: 0 8px;
@@ -1814,7 +1842,7 @@ body {
}:root { }:root {
--perm-caret: #444; --perm-caret: #444;
}/* light */ }/* light */
body.dark-mode { .dark-mode {
--perm-caret: #ccc; --perm-caret: #ccc;
}/* dark */ }/* dark */
@@ -1827,7 +1855,7 @@ body {
background-color 160ms cubic-bezier(.2,.0,.2,1); background-color 160ms cubic-bezier(.2,.0,.2,1);
}:root { }:root {
--toggle-icon-color: #333; --toggle-icon-color: #333;
}body.dark-mode { }.dark-mode {
--toggle-icon-color: #eee; --toggle-icon-color: #eee;
}#zonesToggleFloating .material-icons, }#zonesToggleFloating .material-icons,
#zonesToggleFloating .material-icons-outlined, #zonesToggleFloating .material-icons-outlined,

View File

@@ -2,65 +2,37 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>FileRise</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <script>(function(){try{var s=localStorage.getItem('darkMode');var isDark=(s===null)?(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches):(s==='1'||s==='true');var root=document.documentElement;root.setAttribute('data-theme',isDark?'dark':'light');root.classList.toggle('dark-mode',isDark);var bg=isDark?'#121212':'#ffffff';root.style.backgroundColor=bg;root.style.colorScheme=isDark?'dark':'light';root.style.setProperty('--pre-bg',bg);var m=document.querySelector('meta[name="theme-color"]');if(m)m.setAttribute('content',bg);}catch(e){}})();</script>
<title>FileRise</title> <style id="pretheme-css">
html,body,#loadingOverlay{background:var(--pre-bg,#ffffff) !important;}
<!-- Icons -->
<link rel="icon" type="image/png" href="/assets/logo.png">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<!-- App meta -->
<meta name="description" content="FileRise is a fast, self-hosted file manager with granular per-folder ACLs, drag-and-drop folder moves, WebDAV, tagging, and a clean UI.">
<meta name="csrf-token" content="">
<meta name="share-url" content="">
<meta name="theme-color" content="#0b5ed7">
<!-- Minimal critical CSS only (keeps CSP clean, no inline JS) -->
<style>
.main-wrapper{display:none}
#loadingOverlay{position:fixed;inset:0;background:var(--bg-color,#fff);z-index:9999;display:flex;align-items:center;justify-content:center}
</style> </style>
<link rel="icon" type="image/png" href="/assets/logo.png"><link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<meta name="description" content="FileRise is a fast, self-hosted file manager with granular per-folder ACLs, drag-and-drop folder moves, WebDAV, tagging, and a clean UI.">
<meta name="csrf-token" content=""><meta name="share-url" content=""><meta name="theme-color" content="#0b5ed7"><meta name="color-scheme" content="light dark">
<!-- CSS: preload, then promote via tiny external JS (no inline onload) -->
<link rel="preload" as="style" href="/vendor/bootstrap/4.5.2/bootstrap.min.css?v={{APP_QVER}}">
<link rel="preload" as="style" href="/css/styles.css?v={{APP_QVER}}">
<!-- Fonts: preload only those used above the fold -->
<link rel="preload" href="/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2?v={{APP_QVER}}" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2?v={{APP_QVER}}" as="font" type="font/woff2" crossorigin>
<!-- Do NOT preload material icons unless needed above the fold -->
<!-- Non-blocking stylesheet promotion (external to satisfy CSP) -->
<script src="/js/defer-css.js?v={{APP_QVER}}" defer></script>
<!-- Critical CSS -->
<!-- Base CSS as a fallback if JS is disabled --> <link rel="stylesheet" href="/vendor/bootstrap/4.5.2/bootstrap.min.css?v={{APP_QVER}}">
<noscript> <link rel="stylesheet" href="/css/styles.css?v={{APP_QVER}}">
<link rel="stylesheet" href="/vendor/bootstrap/4.5.2/bootstrap.min.css?v={{APP_QVER}}"> <link rel="stylesheet" href="/css/vendor/roboto.css?v={{APP_QVER}}">
<link rel="stylesheet" href="/css/styles.css?v={{APP_QVER}}">
</noscript> <!-- Fonts (ok to keep as real preloads) -->
<link rel="preload" as="font" href="/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2?v={{APP_QVER}}" type="font/woff2" crossorigin>
<!-- Preload font CSS (non-blocking) --> <link rel="preload" as="font" href="/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2?v={{APP_QVER}}" type="font/woff2" crossorigin>
<link rel="preload" as="style" href="/css/vendor/roboto.css?v={{APP_QVER}}">
<link rel="preload" as="style" href="/css/vendor/material-icons.css?v={{APP_QVER}}"> <!-- Vendor & version (deferred) -->
<!-- Vendor JS (keep defer; theyre not modules) -->
<script src="/vendor/dompurify/2.4.0/purify.min.js?v={{APP_QVER}}" defer></script> <script src="/vendor/dompurify/2.4.0/purify.min.js?v={{APP_QVER}}" defer></script>
<!-- IMPORTANT: Remove CodeMirror here; lazy-load it inside your editor route/module. -->
<!-- Version marker (non-blocking) -->
<script src="/js/version.js?v={{APP_QVER}}" defer></script> <script src="/js/version.js?v={{APP_QVER}}" defer></script>
<!-- App entry -->
<!-- App entry: start fetching early, execute after parse --> <link rel="modulepreload" href="/js/main.js?v={{APP_QVER}}"><script type="module" src="/js/main.js?v={{APP_QVER}}"></script>
<link rel="modulepreload" href="/js/main.js?v={{APP_QVER}}"> </head>
<script type="module" src="/js/main.js?v={{APP_QVER}}"></script>
</head>
<body> <body>
<div id="appRoot" style="visibility:hidden">
<header class="header-container"> <header class="header-container">
<div class="header-left"> <div class="header-left">
<a href="index.html"> <a href="index.html">
<div class="header-logo"> <div class="header-logo">
@@ -68,19 +40,21 @@
src="/assets/logo.svg?v={{APP_QVER}}" src="/assets/logo.svg?v={{APP_QVER}}"
alt="FileRise" alt="FileRise"
class="logo" class="logo"
width="50" height="50" width="50"
height="50"
decoding="async" decoding="async"
fetchpriority="low" fetchpriority="high"
/> />
</div> </div>
</a> </a>
</div> </div>
<div class="header-title"> <div class="header-title">
<h1 data-i18n-key="header_title">FileRise</h1> <h1>FileRise</h1>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;"> <div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;">
<!-- Your header drop zone -->
<div id="headerDropArea" class="header-drop-zone"></div> <div id="headerDropArea" class="header-drop-zone"></div>
<div class="header-buttons"> <div class="header-buttons">
<button id="changePasswordBtn" data-i18n-title="change_password" style="display: none;"> <button id="changePasswordBtn" data-i18n-title="change_password" style="display: none;">
@@ -99,7 +73,7 @@
<!-- Trash items will be loaded here --> <!-- Trash items will be loaded here -->
</div> </div>
<div style="text-align: right;"> <div style="text-align: right;">
<button id="restoreSelectedBtn" class="btn btn-primary" data-i18n-key="restore_selected">Restore <button id="restoreSelectedBtn" class="btn btn-primary" data-i18n-key="restore_selected" style="display: none;">Restore
Selected</button> Selected</button>
<button id="restoreAllBtn" class="btn btn-secondary" data-i18n-key="restore_all">Restore All</button> <button id="restoreAllBtn" class="btn btn-secondary" data-i18n-key="restore_all">Restore All</button>
<button id="deleteTrashSelectedBtn" class="btn btn-warning" data-i18n-key="delete_selected_trash">Delete <button id="deleteTrashSelectedBtn" class="btn btn-warning" data-i18n-key="delete_selected_trash">Delete
@@ -115,7 +89,7 @@
<button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;"> <button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;">
<i class="material-icons">person_remove</i> <i class="material-icons">person_remove</i>
</button> </button>
<button id="darkModeToggle" class="btn-icon" aria-label="Toggle dark mode"> <button id="darkModeToggle" class="btn-icon" aria-label="Toggle dark mode" hidden>
<span class="material-icons" id="darkModeIcon"> <span class="material-icons" id="darkModeIcon">
dark_mode dark_mode
</span> </span>
@@ -124,15 +98,18 @@
</div> </div>
</div> </div>
</header> </header>
<div id="loadingOverlay"></div> <div id="loadingOverlay"></div>
<!-- Custom Toast Container --> <!-- Custom Toast Container -->
<div id="customToast"></div> <div id="customToast"></div>
<div id="hiddenCardsContainer" style="display:none;"></div> <div id="hiddenCardsContainer" style="display:none;"></div>
<main id="main"> <main id="main" hidden>
<div class="row mt-4" id="loginForm"> <div class="row mt-4" id="loginForm">
<div class="col-12"> <div class="col-12">
<div id="loginBox" class="login-box">
<div id="fr-login-tip" class="alert alert-info login-hint" role="status" aria-live="polite" style="display:none;"></div>
<form id="authForm" method="post"> <form id="authForm" method="post">
<div class="form-group"> <div class="form-group">
<label for="loginUsername" data-i18n-key="user">User:</label> <label for="loginUsername" data-i18n-key="user">User:</label>
@@ -158,13 +135,14 @@
HTTP HTTP
Login</a> Login</a>
</div> </div>
<div>
</div> </div>
</div> </div>
</main> </main>
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login --> <!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
<div class="main-wrapper"> <div class="main-wrapper" hidden>
<!-- Sidebar Drop Zone: Hidden until you drag a card (display controlled by JS) --> <!-- Sidebar Drop Zone: Hidden until you drag a card (display controlled by JS) -->
<div id="sidebarDropArea" class="drop-target-sidebar"></div> <div id="sidebarDropArea" class="drop-target-sidebar"></div>
<!-- Main Column --> <!-- Main Column -->
@@ -505,7 +483,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</body> </body>
</html> </html>

View File

@@ -170,9 +170,9 @@ async function safeJson(res) {
max-width: none !important; max-width: none !important;
} }
} }
body.dark-mode #adminPanelModal .modal-content { background:#2c2c2c !important; color:#e0e0e0 !important; border-color:#555 !important; } .dark-mode #adminPanelModal .modal-content { background:#2c2c2c !important; color:#e0e0e0 !important; border-color:#555 !important; }
body.dark-mode .form-control { background-color:#333; border-color:#555; color:#eee; } .dark-mode .form-control { background-color:#333; border-color:#555; color:#eee; }
body.dark-mode .form-control::placeholder { color:#888; } .dark-mode .form-control::placeholder { color:#888; }
.section-header { .section-header {
background:#f5f5f5; padding:10px 15px; cursor:pointer; border-radius:4px; font-weight:bold; background:#f5f5f5; padding:10px 15px; cursor:pointer; border-radius:4px; font-weight:bold;
@@ -181,8 +181,8 @@ async function safeJson(res) {
.section-header:first-of-type { margin-top:0; } .section-header:first-of-type { margin-top:0; }
.section-header.collapsed .material-icons { transform:rotate(-90deg); } .section-header.collapsed .material-icons { transform:rotate(-90deg); }
.section-header .material-icons { transition:transform .3s; color:#444; } .section-header .material-icons { transition:transform .3s; color:#444; }
body.dark-mode .section-header { background:#3a3a3a; color:#eee; } .dark-mode .section-header { background:#3a3a3a; color:#eee; }
body.dark-mode .section-header .material-icons { color:#ccc; } .dark-mode .section-header .material-icons { color:#ccc; }
.section-content { display:none; margin-left:20px; margin-top:8px; margin-bottom:8px; } .section-content { display:none; margin-left:20px; margin-top:8px; margin-bottom:8px; }
@@ -193,7 +193,7 @@ async function safeJson(res) {
border:2px solid transparent; transition:all .3s; border:2px solid transparent; transition:all .3s;
} }
#adminPanelModal .editor-close-btn:hover { color:#fff; background:#ff4d4d; box-shadow:0 0 6px rgba(255,77,77,.8); transform:scale(1.05); } #adminPanelModal .editor-close-btn:hover { color:#fff; background:#ff4d4d; box-shadow:0 0 6px rgba(255,77,77,.8); transform:scale(1.05); }
body.dark-mode #adminPanelModal .editor-close-btn { background:rgba(0,0,0,0.6); color:#ff4d4d; } .dark-mode #adminPanelModal .editor-close-btn { background:rgba(0,0,0,0.6); color:#ff4d4d; }
.action-row { display:flex; justify-content:space-between; margin-top:15px; } .action-row { display:flex; justify-content:space-between; margin-top:15px; }
@@ -210,7 +210,7 @@ async function safeJson(res) {
border-radius: 6px; border-radius: 6px;
padding: 0; padding: 0;
} }
body.dark-mode .folder-access-list { border-color:#555; } .dark-mode .folder-access-list { border-color:#555; }
.folder-access-header, .folder-access-header,
.folder-access-row { .folder-access-row {
@@ -228,7 +228,7 @@ async function safeJson(res) {
font-weight: 700; font-weight: 700;
border-bottom: 1px solid rgba(0,0,0,0.12); border-bottom: 1px solid rgba(0,0,0,0.12);
} }
body.dark-mode .folder-access-header { background:#2c2c2c; } .dark-mode .folder-access-header { background:#2c2c2c; }
.folder-access-row { border-bottom: 1px solid rgba(0,0,0,0.06); } .folder-access-row { border-bottom: 1px solid rgba(0,0,0,0.06); }
.folder-access-row:last-child { border-bottom: none; } .folder-access-row:last-child { border-bottom: none; }
@@ -257,8 +257,8 @@ async function safeJson(res) {
color: #2064ff; color: #2064ff;
margin-left: 6px; margin-left: 6px;
} }
body.dark-mode .inherited-row { background: rgba(32,132,255,0.12); } .dark-mode .inherited-row { background: rgba(32,132,255,0.12); }
body.dark-mode .inherited-tag { background: rgba(32,132,255,0.2); color: #89b3ff; } .dark-mode .inherited-tag { background: rgba(32,132,255,0.2); color: #89b3ff; }
@media (max-width: 900px) { @media (max-width: 900px) {
.folder-access-list { --col-perm: 72px; --col-folder-min: 240px; } .folder-access-list { --col-perm: 72px; --col-folder-min: 240px; }
@@ -274,7 +274,7 @@ async function safeJson(res) {
/* nicer thin scrollbar (supported browsers) */ /* nicer thin scrollbar (supported browsers) */
.folder-cell::-webkit-scrollbar{ height:8px; } .folder-cell::-webkit-scrollbar{ height:8px; }
.folder-cell::-webkit-scrollbar-thumb{ background:rgba(0,0,0,.25); border-radius:4px; } .folder-cell::-webkit-scrollbar-thumb{ background:rgba(0,0,0,.25); border-radius:4px; }
body.dark-mode .folder-cell::-webkit-scrollbar-thumb{ background:rgba(255,255,255,.25); } .dark-mode .folder-cell::-webkit-scrollbar-thumb{ background:rgba(255,255,255,.25); }
/* Badge now doesn't clip; let the wrapper handle scroll */ /* Badge now doesn't clip; let the wrapper handle scroll */
.folder-badge{ .folder-badge{

View File

@@ -1,20 +1,31 @@
// Promote any preloaded styles to real stylesheets without inline handlers (CSP-safe) // /public/js/defer-css.js
document.addEventListener('DOMContentLoaded', () => { // Promote preloaded styles to real stylesheets (CSP-safe) and expose a load promise.
// Promote any preloaded core CSS (function () {
document.querySelectorAll('link[rel="preload"][as="style"][href]').forEach(link => { if (window.__CSS_PROMISE__) return;
const href = link.getAttribute('href');
if ([...document.querySelectorAll('link[rel="stylesheet"]')]
.some(s => s.getAttribute('href') === href)) return;
const sheet = document.createElement('link');
sheet.rel = 'stylesheet';
sheet.href = href;
document.head.appendChild(sheet);
});
var loads = [];
// Optionally load non-critical icon/extra font CSS after first paint: // Promote <link rel="preload" as="style"> IN-PLACE
const extra = document.createElement('link'); var preloads = document.querySelectorAll('link[rel="preload"][as="style"]');
extra.rel = 'stylesheet'; for (var i = 0; i < preloads.length; i++) {
extra.href = '/css/vendor/material-icons.css?v={{APP_QVER}}'; var l = preloads[i];
document.head.appendChild(extra); // resolve when it finishes loading as a stylesheet
}); loads.push(new Promise(function (res) { l.addEventListener('load', res, { once: true }); }));
l.rel = 'stylesheet';
if (!l.media || l.media === 'print') l.media = 'all'; // be explicit
l.removeAttribute('as'); // keep some engines happy about "used" preload
}
// Also wait for any existing <link rel="stylesheet"> that haven't finished yet
var styles = document.querySelectorAll('link[rel="stylesheet"]');
for (var j = 0; j < styles.length; j++) {
var s = styles[j];
if (s.sheet) continue; // already applied
loads.push(new Promise(function (res) { s.addEventListener('load', res, { once: true }); }));
}
// Safari quirk: nudge layout so promoted sheets apply immediately
void document.documentElement.offsetHeight;
window.__CSS_PROMISE__ = Promise.all(loads);
})();

View File

@@ -2,6 +2,7 @@
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}'; import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
import { loadFileList } from './fileListView.js?v={{APP_QVER}}'; import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
import { t } from './i18n.js?v={{APP_QVER}}'; import { t } from './i18n.js?v={{APP_QVER}}';
import { buildPreviewUrl } from './filePreview.js?v={{APP_QVER}}';
// thresholds for editor behavior // thresholds for editor behavior
const EDITOR_PLAIN_THRESHOLD = 5 * 1024 * 1024; // >5 MiB => force plain text, lighter settings const EDITOR_PLAIN_THRESHOLD = 5 * 1024 * 1024; // >5 MiB => force plain text, lighter settings
@@ -14,7 +15,7 @@ const CM_BASE = "/vendor/codemirror/5.65.5/";
const coreUrl = (p) => `${CM_BASE}${p}?v={{APP_QVER}}`; const coreUrl = (p) => `${CM_BASE}${p}?v={{APP_QVER}}`;
const CORE = { const CORE = {
js: coreUrl("codemirror.min.js"), js: coreUrl("codemirror.min.js"),
css: coreUrl("codemirror.min.css"), css: coreUrl("codemirror.min.css"),
themeCss: coreUrl("theme/material-darker.min.css"), themeCss: coreUrl("theme/material-darker.min.css"),
}; };
@@ -22,30 +23,30 @@ const CORE = {
// Which mode file to load for a given name/mime // Which mode file to load for a given name/mime
const MODE_URL = { const MODE_URL = {
// core/common // core/common
"xml": "mode/xml/xml.min.js?v={{APP_QVER}}", "xml": "mode/xml/xml.min.js?v={{APP_QVER}}",
"css": "mode/css/css.min.js?v={{APP_QVER}}", "css": "mode/css/css.min.js?v={{APP_QVER}}",
"javascript": "mode/javascript/javascript.min.js?v={{APP_QVER}}", "javascript": "mode/javascript/javascript.min.js?v={{APP_QVER}}",
// meta / combos // meta / combos
"htmlmixed": "mode/htmlmixed/htmlmixed.min.js?v={{APP_QVER}}", "htmlmixed": "mode/htmlmixed/htmlmixed.min.js?v={{APP_QVER}}",
"application/x-httpd-php": "mode/php/php.min.js?v={{APP_QVER}}", "application/x-httpd-php": "mode/php/php.min.js?v={{APP_QVER}}",
// docs / data // docs / data
"markdown": "mode/markdown/markdown.min.js?v={{APP_QVER}}", "markdown": "mode/markdown/markdown.min.js?v={{APP_QVER}}",
"yaml": "mode/yaml/yaml.min.js?v={{APP_QVER}}", "yaml": "mode/yaml/yaml.min.js?v={{APP_QVER}}",
"properties": "mode/properties/properties.min.js?v={{APP_QVER}}", "properties": "mode/properties/properties.min.js?v={{APP_QVER}}",
"sql": "mode/sql/sql.min.js?v={{APP_QVER}}", "sql": "mode/sql/sql.min.js?v={{APP_QVER}}",
// shells // shells
"shell": "mode/shell/shell.min.js?v={{APP_QVER}}", "shell": "mode/shell/shell.min.js?v={{APP_QVER}}",
// languages // languages
"python": "mode/python/python.min.js?v={{APP_QVER}}", "python": "mode/python/python.min.js?v={{APP_QVER}}",
"text/x-csrc": "mode/clike/clike.min.js?v={{APP_QVER}}", "text/x-csrc": "mode/clike/clike.min.js?v={{APP_QVER}}",
"text/x-c++src": "mode/clike/clike.min.js?v={{APP_QVER}}", "text/x-c++src": "mode/clike/clike.min.js?v={{APP_QVER}}",
"text/x-java": "mode/clike/clike.min.js?v={{APP_QVER}}", "text/x-java": "mode/clike/clike.min.js?v={{APP_QVER}}",
"text/x-csharp": "mode/clike/clike.min.js?v={{APP_QVER}}", "text/x-csharp": "mode/clike/clike.min.js?v={{APP_QVER}}",
"text/x-kotlin": "mode/clike/clike.min.js?v={{APP_QVER}}" "text/x-kotlin": "mode/clike/clike.min.js?v={{APP_QVER}}"
}; };
// Mode dependency graph // Mode dependency graph
@@ -201,23 +202,37 @@ export function editFile(fileName, folder) {
if (existingEditor) existingEditor.remove(); if (existingEditor) existingEditor.remove();
const folderUsed = folder || window.currentFolder || "root"; const folderUsed = folder || window.currentFolder || "root";
const folderPath = folderUsed === "root" const fileUrl = buildPreviewUrl(folderUsed, fileName);
? "uploads/"
: "uploads/" + folderUsed.split("/").map(encodeURIComponent).join("/") + "/";
const fileUrl = folderPath + encodeURIComponent(fileName) + "?t=" + new Date().getTime();
fetch(fileUrl, { method: "HEAD" }) // Probe size safely via API. Prefer HEAD; if missing Content-Length, fall back to a 1-byte Range GET.
.then(response => { async function probeSize(url) {
const lenHeader = response.headers.get("content-length") ?? response.headers.get("Content-Length"); try {
const sizeBytes = lenHeader ? parseInt(lenHeader, 10) : null; const h = await fetch(url, { method: "HEAD", credentials: "include" });
const len = h.headers.get("content-length") ?? h.headers.get("Content-Length");
if (len && !Number.isNaN(parseInt(len, 10))) return parseInt(len, 10);
} catch { }
try {
const r = await fetch(url, {
method: "GET",
headers: { Range: "bytes=0-0" },
credentials: "include"
});
// Content-Range: bytes 0-0/12345
const cr = r.headers.get("content-range") ?? r.headers.get("Content-Range");
const m = cr && cr.match(/\/(\d+)\s*$/);
if (m) return parseInt(m[1], 10);
} catch { }
return null;
}
probeSize(fileUrl)
.then(sizeBytes => {
if (sizeBytes !== null && sizeBytes > EDITOR_BLOCK_THRESHOLD) { if (sizeBytes !== null && sizeBytes > EDITOR_BLOCK_THRESHOLD) {
showToast("This file is larger than 10 MB and cannot be edited in the browser."); showToast("This file is larger than 10 MB and cannot be edited in the browser.");
throw new Error("File too large."); throw new Error("File too large.");
} }
return response; return fetch(fileUrl, { credentials: "include" });
}) })
.then(() => fetch(fileUrl))
.then(response => { .then(response => {
if (!response.ok) throw new Error("HTTP error! Status: " + response.status); if (!response.ok) throw new Error("HTTP error! Status: " + response.status);
const lenHeader = response.headers.get("content-length") ?? response.headers.get("Content-Length"); const lenHeader = response.headers.get("content-length") ?? response.headers.get("Content-Length");
@@ -269,8 +284,8 @@ export function editFile(fileName, folder) {
// Keep buttons responsive even before editor exists // Keep buttons responsive even before editor exists
const decBtn = document.getElementById("decreaseFont"); const decBtn = document.getElementById("decreaseFont");
const incBtn = document.getElementById("increaseFont"); const incBtn = document.getElementById("increaseFont");
decBtn.addEventListener("click", () => {}); decBtn.addEventListener("click", () => { });
incBtn.addEventListener("click", () => {}); incBtn.addEventListener("click", () => { });
// Theme + mode selection // Theme + mode selection
const isDarkMode = document.body.classList.contains("dark-mode"); const isDarkMode = document.body.classList.contains("dark-mode");

View File

@@ -1,7 +1,7 @@
// fileMenu.js // fileMenu.js
import { updateRowHighlight, showToast } from './domUtils.js?v={{APP_QVER}}'; import { updateRowHighlight, showToast } from './domUtils.js?v={{APP_QVER}}';
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile, openCreateFileModal } from './fileActions.js?v={{APP_QVER}}'; import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile, openCreateFileModal } from './fileActions.js?v={{APP_QVER}}';
import { previewFile } from './filePreview.js?v={{APP_QVER}}'; import { previewFile, buildPreviewUrl } from './filePreview.js?v={{APP_QVER}}';
import { editFile } from './fileEditor.js?v={{APP_QVER}}'; import { editFile } from './fileEditor.js?v={{APP_QVER}}';
import { canEditFile, fileData } from './fileListView.js?v={{APP_QVER}}'; import { canEditFile, fileData } from './fileListView.js?v={{APP_QVER}}';
import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}'; import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}';
@@ -39,11 +39,11 @@ export function showFileContextMenu(x, y, menuItems) {
}); });
menu.appendChild(menuItem); menu.appendChild(menuItem);
}); });
menu.style.left = x + "px"; menu.style.left = x + "px";
menu.style.top = y + "px"; menu.style.top = y + "px";
menu.style.display = "block"; menu.style.display = "block";
const menuRect = menu.getBoundingClientRect(); const menuRect = menu.getBoundingClientRect();
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight;
if (menuRect.bottom > viewportHeight) { if (menuRect.bottom > viewportHeight) {
@@ -62,7 +62,7 @@ export function hideFileContextMenu() {
export function fileListContextMenuHandler(e) { export function fileListContextMenuHandler(e) {
e.preventDefault(); e.preventDefault();
let row = e.target.closest("tr"); let row = e.target.closest("tr");
if (row) { if (row) {
const checkbox = row.querySelector(".file-checkbox"); const checkbox = row.querySelector(".file-checkbox");
@@ -71,9 +71,9 @@ export function fileListContextMenuHandler(e) {
updateRowHighlight(checkbox); updateRowHighlight(checkbox);
} }
} }
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("create_file"), action: () => openCreateFileModal() },
{ label: t("delete_selected"), action: () => { handleDeleteSelected(new Event("click")); } }, { label: t("delete_selected"), action: () => { handleDeleteSelected(new Event("click")); } },
@@ -81,14 +81,14 @@ export function fileListContextMenuHandler(e) {
{ label: t("move_selected"), action: () => { handleMoveSelected(new Event("click")); } }, { label: t("move_selected"), action: () => { handleMoveSelected(new Event("click")); } },
{ label: t("download_zip"), action: () => { handleDownloadZipSelected(new Event("click")); } } { label: t("download_zip"), action: () => { handleDownloadZipSelected(new Event("click")); } }
]; ];
if (selected.some(name => name.toLowerCase().endsWith(".zip"))) { if (selected.some(name => name.toLowerCase().endsWith(".zip"))) {
menuItems.push({ menuItems.push({
label: t("extract_zip"), label: t("extract_zip"),
action: () => { handleExtractZipSelected(new Event("click")); } action: () => { handleExtractZipSelected(new Event("click")); }
}); });
} }
if (selected.length > 1) { if (selected.length > 1) {
menuItems.push({ menuItems.push({
label: t("tag_selected"), label: t("tag_selected"),
@@ -100,36 +100,33 @@ export function fileListContextMenuHandler(e) {
} }
else if (selected.length === 1) { else if (selected.length === 1) {
const file = fileData.find(f => f.name === selected[0]); const file = fileData.find(f => f.name === selected[0]);
menuItems.push({ menuItems.push({
label: t("preview"), label: t("preview"),
action: () => { action: () => {
const folder = window.currentFolder || "root"; const folder = window.currentFolder || "root";
const folderPath = folder === "root" previewFile(buildPreviewUrl(folder, file.name), file.name);
? "uploads/"
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
previewFile(folderPath + encodeURIComponent(file.name) + "?t=" + new Date().getTime(), file.name);
} }
}); });
if (canEditFile(file.name)) { if (canEditFile(file.name)) {
menuItems.push({ menuItems.push({
label: t("edit"), label: t("edit"),
action: () => { editFile(selected[0], window.currentFolder); } action: () => { editFile(selected[0], window.currentFolder); }
}); });
} }
menuItems.push({ menuItems.push({
label: t("rename"), label: t("rename"),
action: () => { renameFile(selected[0], window.currentFolder); } action: () => { renameFile(selected[0], window.currentFolder); }
}); });
menuItems.push({ menuItems.push({
label: t("tag_file"), label: t("tag_file"),
action: () => { openTagModal(file); } action: () => { openTagModal(file); }
}); });
} }
showFileContextMenu(e.clientX, e.clientY, menuItems); showFileContextMenu(e.clientX, e.clientY, menuItems);
} }
@@ -140,7 +137,7 @@ export function bindFileListContextMenu() {
} }
} }
document.addEventListener("click", function(e) { document.addEventListener("click", function (e) {
const menu = document.getElementById("fileContextMenu"); const menu = document.getElementById("fileContextMenu");
if (menu && menu.style.display === "block") { if (menu && menu.style.display === "block") {
hideFileContextMenu(); hideFileContextMenu();
@@ -148,9 +145,9 @@ document.addEventListener("click", function(e) {
}); });
// Rebind context menu after file table render. // Rebind context menu after file table render.
(function() { (function () {
const originalRenderFileTable = window.renderFileTable; const originalRenderFileTable = window.renderFileTable;
window.renderFileTable = function(folder) { window.renderFileTable = function (folder) {
originalRenderFileTable(folder); originalRenderFileTable(folder);
bindFileListContextMenu(); bindFileListContextMenu();
}; };

View File

@@ -3,6 +3,12 @@ import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
import { fileData } from './fileListView.js?v={{APP_QVER}}'; import { fileData } from './fileListView.js?v={{APP_QVER}}';
import { t } from './i18n.js?v={{APP_QVER}}'; import { t } from './i18n.js?v={{APP_QVER}}';
// Build a preview URL that always goes through the API layer (respects ACLs/UPLOAD_DIR)
export function buildPreviewUrl(folder, name) {
const f = (!folder || folder === '') ? 'root' : String(folder);
return `/api/file/download.php?folder=${encodeURIComponent(f)}&file=${encodeURIComponent(name)}&inline=1&t=${Date.now()}`;
}
export function openShareModal(file, folder) { export function openShareModal(file, folder) {
// Remove any existing modal // Remove any existing modal
const existing = document.getElementById("shareModal"); const existing = document.getElementById("shareModal");
@@ -92,10 +98,10 @@ export function openShareModal(file, folder) {
if (sel.value === "custom") { if (sel.value === "custom") {
value = parseInt(document.getElementById("customExpirationValue").value, 10); value = parseInt(document.getElementById("customExpirationValue").value, 10);
unit = document.getElementById("customExpirationUnit").value; unit = document.getElementById("customExpirationUnit").value;
} else { } else {
value = parseInt(sel.value, 10); value = parseInt(sel.value, 10);
unit = "minutes"; unit = "minutes";
} }
const password = document.getElementById("sharePassword").value; const password = document.getElementById("sharePassword").value;
@@ -115,20 +121,20 @@ export function openShareModal(file, folder) {
password password
}) })
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (data.token) { if (data.token) {
const url = `${window.location.origin}/api/file/share.php?token=${encodeURIComponent(data.token)}`; const url = `${window.location.origin}/api/file/share.php?token=${encodeURIComponent(data.token)}`;
document.getElementById("shareLinkInput").value = url; document.getElementById("shareLinkInput").value = url;
document.getElementById("shareLinkDisplay").style.display = "block"; document.getElementById("shareLinkDisplay").style.display = "block";
} else { } else {
showToast(t("error_generating_share") + ": " + (data.error||"Unknown")); showToast(t("error_generating_share") + ": " + (data.error || "Unknown"));
} }
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
showToast(t("error_generating_share")); showToast(t("error_generating_share"));
}); });
}); });
// Copy to clipboard // Copy to clipboard
@@ -272,10 +278,7 @@ export function previewFile(fileUrl, fileName) {
modal.galleryCurrentIndex = (modal.galleryCurrentIndex - 1 + modal.galleryImages.length) % modal.galleryImages.length; modal.galleryCurrentIndex = (modal.galleryCurrentIndex - 1 + modal.galleryImages.length) % modal.galleryImages.length;
let newFile = modal.galleryImages[modal.galleryCurrentIndex]; let newFile = modal.galleryImages[modal.galleryCurrentIndex];
modal.querySelector("h4").textContent = newFile.name; modal.querySelector("h4").textContent = newFile.name;
img.src = ((window.currentFolder === "root") img.src = buildPreviewUrl(window.currentFolder || 'root', newFile.name);
? "uploads/"
: "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/")
+ encodeURIComponent(newFile.name) + "?t=" + new Date().getTime();
// Reset transforms. // Reset transforms.
img.dataset.scale = 1; img.dataset.scale = 1;
img.dataset.rotate = 0; img.dataset.rotate = 0;
@@ -355,10 +358,7 @@ export function previewFile(fileUrl, fileName) {
modal.galleryCurrentIndex = (modal.galleryCurrentIndex + 1) % modal.galleryImages.length; modal.galleryCurrentIndex = (modal.galleryCurrentIndex + 1) % modal.galleryImages.length;
let newFile = modal.galleryImages[modal.galleryCurrentIndex]; let newFile = modal.galleryImages[modal.galleryCurrentIndex];
modal.querySelector("h4").textContent = newFile.name; modal.querySelector("h4").textContent = newFile.name;
img.src = ((window.currentFolder === "root") img.src = buildPreviewUrl(window.currentFolder || 'root', newFile.name);
? "uploads/"
: "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/")
+ encodeURIComponent(newFile.name) + "?t=" + new Date().getTime();
// Reset transforms. // Reset transforms.
img.dataset.scale = 1; img.dataset.scale = 1;
img.dataset.rotate = 0; img.dataset.rotate = 0;
@@ -416,26 +416,26 @@ export function previewFile(fileUrl, fileName) {
} }
} else { } else {
// Handle non-image file previews. // Handle non-image file previews.
if (extension === "pdf") { if (extension === "pdf") {
// build a cachebusted URL // build a cachebusted URL
const separator = fileUrl.includes('?') ? '&' : '?'; const separator = fileUrl.includes('?') ? '&' : '?';
const urlWithTs = fileUrl + separator + 't=' + Date.now(); const urlWithTs = fileUrl + separator + 't=' + Date.now();
// open in a new tab (avoids CSP frame-ancestors) // open in a new tab (avoids CSP frame-ancestors)
window.open(urlWithTs, "_blank"); window.open(urlWithTs, "_blank");
// tear down the just-created modal // tear down the just-created modal
const modal = document.getElementById("filePreviewModal"); const modal = document.getElementById("filePreviewModal");
if (modal) modal.remove(); if (modal) modal.remove();
// stop further preview logic // stop further preview logic
return; return;
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) { } else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) {
const video = document.createElement("video"); const video = document.createElement("video");
video.src = fileUrl; video.src = fileUrl;
video.controls = true; video.controls = true;
video.className = "image-modal-img"; video.className = "image-modal-img";
const progressKey = 'videoProgress-' + fileUrl; const progressKey = 'videoProgress-' + fileUrl;
video.addEventListener("loadedmetadata", () => { video.addEventListener("loadedmetadata", () => {
const savedTime = localStorage.getItem(progressKey); const savedTime = localStorage.getItem(progressKey);

View File

@@ -67,32 +67,25 @@ function isDemoHost() {
} }
function showLoginTip(message) { function showLoginTip(message) {
const form = document.getElementById('loginForm'); const tip = document.getElementById('fr-login-tip');
if (!form) return; if (!tip) return;
tip.innerHTML = ''; // clear
let tip = document.getElementById('fr-login-tip'); if (message) tip.append(document.createTextNode(message));
if (!tip) { if (location.hostname.replace(/^www\./, '') === 'demo.filerise.net') {
tip = document.createElement('div'); const line = document.createElement('div'); line.style.marginTop = '6px';
tip.id = 'fr-login-tip'; const mk = t => { const k = document.createElement('code'); k.textContent = t; return k; };
tip.className = 'alert alert-info'; // fine even without Bootstrap line.append(document.createTextNode('Demo login — user: '), mk('demo'),
tip.style.marginTop = '8px'; document.createTextNode(' · pass: '), mk('demo'));
form.prepend(tip);
}
// Clear & rebuild so we can add the demo hint cleanly
tip.textContent = '';
tip.append(document.createTextNode(message || ''));
if (isDemoHost()) {
const line = document.createElement('div');
line.style.marginTop = '6px';
const mk = (txt) => { const k = document.createElement('code'); k.textContent = txt; return k; };
line.append(
document.createTextNode('Demo login — user: '), mk('demo'),
document.createTextNode(' · pass: '), mk('demo')
);
tip.append(line); tip.append(line);
} }
tip.style.display = 'block'; // reveal without shifting layout
}
async function hideOverlaySmoothly(overlay) {
if (!overlay) return;
try { await document.fonts?.ready; } catch { }
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
overlay.style.display = 'none';
} }
function wireModalEnterDefault() { function wireModalEnterDefault() {
@@ -322,7 +315,6 @@ function applyDarkMode({ fromSystemChange = false } = {}) {
let stored = null; let stored = null;
try { stored = localStorage.getItem('darkMode'); } catch { } try { stored = localStorage.getItem('darkMode'); } catch { }
// If no stored pref, fall back to system
let isDark = (stored === null) let isDark = (stored === null)
? (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
: (stored === '1' || stored === 'true'); : (stored === '1' || stored === 'true');
@@ -336,15 +328,26 @@ function applyDarkMode({ fromSystemChange = false } = {}) {
el.setAttribute('data-theme', isDark ? 'dark' : 'light'); el.setAttribute('data-theme', isDark ? 'dark' : 'light');
}); });
// keep UA chrome & bg consistent post-toggle
const bg = isDark ? '#121212' : '#ffffff';
root.style.backgroundColor = bg;
root.style.colorScheme = isDark ? 'dark' : 'light';
if (body) {
body.style.backgroundColor = bg;
body.style.colorScheme = isDark ? 'dark' : 'light';
}
const mt = document.querySelector('meta[name="theme-color"]');
if (mt) mt.content = bg;
const mcs = document.querySelector('meta[name="color-scheme"]');
if (mcs) mcs.content = isDark ? 'dark light' : 'light dark';
const btn = document.getElementById('darkModeToggle'); const btn = document.getElementById('darkModeToggle');
const icon = document.getElementById('darkModeIcon'); const icon = document.getElementById('darkModeIcon');
if (icon) icon.textContent = isDark ? 'light_mode' : 'dark_mode'; if (icon) icon.textContent = isDark ? 'light_mode' : 'dark_mode';
if (btn) { if (btn) {
const ttOn = (typeof t === 'function' ? t('switch_to_dark_mode') : 'Switch to dark mode'); const ttOn = (typeof t === 'function' ? t('switch_to_dark_mode') : 'Switch to dark mode');
const ttOff = (typeof t === 'function' ? t('switch_to_light_mode') : 'Switch to light mode'); const ttOff = (typeof t === 'function' ? t('switch_to_light_mode') : 'Switch to light mode');
const aria = (typeof t === 'function' ? (isDark ? t('light_mode') : t('dark_mode')) : (isDark ? 'Light mode' : 'Dark mode')); const aria = (typeof t === 'function' ? (isDark ? t('light_mode') : t('dark_mode')) : (isDark ? 'Light mode' : 'Dark mode'));
btn.classList.toggle('active', isDark); btn.classList.toggle('active', isDark);
btn.setAttribute('aria-label', aria); btn.setAttribute('aria-label', aria);
btn.setAttribute('title', isDark ? ttOff : ttOn); btn.setAttribute('title', isDark ? ttOff : ttOn);
@@ -381,6 +384,9 @@ function bindDarkMode() {
// ---------- tiny utils ---------- // ---------- tiny utils ----------
const $ = (s, root = document) => root.querySelector(s); const $ = (s, root = document) => root.querySelector(s);
const $$ = (s, root = document) => Array.from(root.querySelectorAll(s)); const $$ = (s, root = document) => Array.from(root.querySelectorAll(s));
// Safe show/hide that work with both CSS and [hidden]
const unhide = (el) => { if (!el) return; el.removeAttribute('hidden'); el.style.display = ''; };
const hideEl = (el) => { if (!el) return; el.setAttribute('hidden', ''); el.style.display = 'none'; };
const show = (el) => { const show = (el) => {
if (!el) return; if (!el) return;
el.hidden = false; el.classList?.remove('d-none', 'hidden'); el.hidden = false; el.classList?.remove('d-none', 'hidden');
@@ -394,28 +400,88 @@ function bindDarkMode() {
}; };
// ---------- site config / auth ---------- // ---------- site config / auth ----------
function applySiteConfig(cfg) { function applySiteConfig(cfg, { phase = 'final' } = {}) {
try { try {
const title = (cfg && cfg.header_title) ? String(cfg.header_title) : 'FileRise'; const title = (cfg && cfg.header_title) ? String(cfg.header_title) : 'FileRise';
// Always keep <title> correct early (no visual flicker)
document.title = title; document.title = title;
const h1 = document.querySelector('.header-title h1'); if (h1) h1.textContent = title;
// --- Login options (apply in BOTH phases so login page is correct) ---
const lo = (cfg && cfg.loginOptions) ? cfg.loginOptions : {}; const lo = (cfg && cfg.loginOptions) ? cfg.loginOptions : {};
const disableForm = !!lo.disableFormLogin; const disableForm = !!lo.disableFormLogin;
const disableOIDC = !!lo.disableOIDCLogin; const disableOIDC = !!lo.disableOIDCLogin;
const disableBasic = !!lo.disableBasicAuth; const disableBasic = !!lo.disableBasicAuth;
const row = $('#loginForm'); if (row) row.style.display = disableForm ? 'none' : ''; const row = $('#loginForm');
const oidc = $('#oidcLoginBtn'); if (oidc) oidc.style.display = disableOIDC ? 'none' : ''; if (row) {
if (disableForm) {
row.setAttribute('hidden', '');
row.style.display = ''; // don't leave display:none lying around
} else {
row.removeAttribute('hidden');
row.style.display = '';
}
}
const oidc = $('#oidcLoginBtn'); if (oidc) oidc.style.display = disableOIDC ? 'none' : '';
const basic = document.querySelector('a[href="/api/auth/login_basic.php"]'); const basic = document.querySelector('a[href="/api/auth/login_basic.php"]');
if (basic) basic.style.display = disableBasic ? 'none' : ''; if (basic) basic.style.display = disableBasic ? 'none' : '';
// --- Header <h1> only in the FINAL phase (prevents visible flips) ---
if (phase === 'final') {
const h1 = document.querySelector('.header-title h1');
if (h1) {
// prevent i18n or legacy from overwriting it
if (h1.hasAttribute('data-i18n-key')) h1.removeAttribute('data-i18n-key');
if (h1.textContent !== title) h1.textContent = title;
// lock it so late code can't stomp it
if (!h1.__titleLock) {
const mo = new MutationObserver(() => {
if (h1.textContent !== title) h1.textContent = title;
});
mo.observe(h1, { childList: true, characterData: true, subtree: true });
h1.__titleLock = mo;
}
}
}
} catch { } } catch { }
} }
async function readyToReveal() {
// Wait for CSS + fonts so the first revealed frame is fully styled
try { await (window.__CSS_PROMISE__ || Promise.resolve()); } catch { }
try { await document.fonts?.ready; } catch { }
// Give layout one paint to settle
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
}
async function revealAppAndHideOverlay() {
const appRoot = document.getElementById('appRoot');
const overlay = document.getElementById('loadingOverlay');
await readyToReveal();
if (appRoot) appRoot.style.visibility = 'visible';
if (overlay) {
overlay.style.transition = 'opacity .18s ease-out';
overlay.style.opacity = '0';
setTimeout(() => { overlay.style.display = 'none'; }, 220);
}
}
async function loadSiteConfig() { async function loadSiteConfig() {
try { try {
const r = await fetch('/api/siteConfig.php', { credentials: 'include' }); const r = await fetch('/api/siteConfig.php', { credentials: 'include' });
const j = await r.json().catch(() => ({})); applySiteConfig(j); const j = await r.json().catch(() => ({}));
} catch { applySiteConfig({}); } window.__FR_SITE_CFG__ = j || {};
// Early pass: title + login options (skip touching <h1> to avoid flicker)
applySiteConfig(window.__FR_SITE_CFG__, { phase: 'early' });
return window.__FR_SITE_CFG__;
} catch {
window.__FR_SITE_CFG__ = {};
applySiteConfig({}, { phase: 'early' });
return null;
}
} }
async function primeCsrf() { async function primeCsrf() {
try { try {
@@ -665,7 +731,6 @@ function bindDarkMode() {
function forceLoginVisible() { function forceLoginVisible() {
show($('#main')); show($('#main'));
show($('#loginForm')); show($('#loginForm'));
hide($('.main-wrapper'));
const hb = $('.header-buttons'); if (hb) hb.style.visibility = 'hidden'; const hb = $('.header-buttons'); if (hb) hb.style.visibility = 'hidden';
const ov = $('#loadingOverlay'); if (ov) ov.style.display = 'none'; const ov = $('#loadingOverlay'); if (ov) ov.style.display = 'none';
} }
@@ -809,8 +874,7 @@ function bindDarkMode() {
window.__FR_FLAGS.booted = true; window.__FR_FLAGS.booted = true;
ensureToastReady(); ensureToastReady();
// show chrome // show chrome
const wrap = document.querySelector('.main-wrapper'); if (wrap) { wrap.hidden = false; wrap.classList?.remove('d-none', 'hidden'); wrap.style.display = 'block'; }
const lf = document.getElementById('loginForm'); if (lf) lf.style.display = 'none';
const hb = document.querySelector('.header-buttons'); if (hb) hb.style.visibility = 'visible'; const hb = document.querySelector('.header-buttons'); if (hb) hb.style.visibility = 'visible';
const ov = document.getElementById('loadingOverlay'); if (ov) ov.style.display = 'flex'; const ov = document.getElementById('loadingOverlay'); if (ov) ov.style.display = 'flex';
@@ -825,6 +889,9 @@ function bindDarkMode() {
window.__FR_AUTH_STATE = state; window.__FR_AUTH_STATE = state;
} catch { } } catch { }
// authed → heavy boot path
document.body.classList.add('authed');
// 1) i18n (safe) // 1) i18n (safe)
// i18n: honor saved language first, then apply translations // i18n: honor saved language first, then apply translations
try { try {
@@ -840,10 +907,20 @@ function bindDarkMode() {
if (!window.__FR_FLAGS.initialized) { if (!window.__FR_FLAGS.initialized) {
if (typeof app.loadCsrfToken === 'function') await app.loadCsrfToken(); if (typeof app.loadCsrfToken === 'function') await app.loadCsrfToken();
if (typeof app.initializeApp === 'function') app.initializeApp(); if (typeof app.initializeApp === 'function') app.initializeApp();
const darkBtn = document.getElementById('darkModeToggle');
if (darkBtn) {
darkBtn.removeAttribute('hidden');
darkBtn.style.setProperty('display', 'inline-flex', 'important'); // beats any CSS
darkBtn.style.visibility = ''; // just in case
}
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/css/vendor/material-icons.css?v={{APP_QVER}}';
document.head.appendChild(link);
window.__FR_FLAGS.initialized = true; window.__FR_FLAGS.initialized = true;
// Show "Welcome back, <username>!" only once per tab-session
try { try {
if (!sessionStorage.getItem('__fr_welcomed')) { if (!sessionStorage.getItem('__fr_welcomed')) {
const name = (window.__FR_AUTH_STATE?.username) || localStorage.getItem('username') || ''; const name = (window.__FR_AUTH_STATE?.username) || localStorage.getItem('username') || '';
@@ -864,7 +941,7 @@ function bindDarkMode() {
auth.applyProxyBypassUI && auth.applyProxyBypassUI(); auth.applyProxyBypassUI && auth.applyProxyBypassUI();
auth.updateAuthenticatedUI && auth.updateAuthenticatedUI(state); auth.updateAuthenticatedUI && auth.updateAuthenticatedUI(state);
// ⬇️ bind ALL the admin / change-password buttons once // bind ALL the admin / change-password buttons once
if (!window.__FR_FLAGS.wired.authInit && typeof auth.initAuth === 'function') { if (!window.__FR_FLAGS.wired.authInit && typeof auth.initAuth === 'function') {
try { auth.initAuth(); } catch (e) { console.warn('[auth] initAuth failed', e); } try { auth.initAuth(); } catch (e) { console.warn('[auth] initAuth failed', e); }
window.__FR_FLAGS.wired.authInit = true; window.__FR_FLAGS.wired.authInit = true;
@@ -913,36 +990,71 @@ function bindDarkMode() {
// ---------- entry (no flicker: decide state BEFORE showing login) ---------- // ---------- entry (no flicker: decide state BEFORE showing login) ----------
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
if (window.__FR_FLAGS.entryStarted) return; if (window.__FR_FLAGS.entryStarted) return;
window.__FR_FLAGS.entryStarted = true; window.__FR_FLAGS.entryStarted = true;
// Always start clean
document.body.classList.remove('authed');
const overlay = document.getElementById('loadingOverlay');
const wrap = document.querySelector('.main-wrapper'); // app shell
const mainEl = document.getElementById('main'); // contains loginForm
const login = document.getElementById('loginForm');
bindDarkMode(); bindDarkMode();
await loadSiteConfig(); await loadSiteConfig();
const { authed, setup } = await checkAuth(); const { authed, setup } = await checkAuth();
if (setup) { await bootSetupWizard(); return; } if (setup) {
if (authed) { await bootHeavy(); return; } // Setup wizard runs inside app shell
unhide(wrap);
hideEl(login);
await bootSetupWizard();
await revealAppAndHideOverlay();
// login view return;
show(document.querySelector('#main')); }
show(document.querySelector('#loginForm'));
(document.querySelector('.header-buttons') || {}).style && (document.querySelector('.header-buttons').style.visibility = 'hidden'); if (authed) {
const ov = document.getElementById('loadingOverlay'); if (ov) ov.style.display = 'none'; // Authenticated path: show app, hide login
document.body.classList.add('authed');
unhide(wrap); // works whether CSS or [hidden] was used
hideEl(login);
await bootHeavy();
await revealAppAndHideOverlay();
requestAnimationFrame(() => {
const pre = document.getElementById('pretheme-css');
if (pre) pre.remove();
});
return;
}
// ---- NOT AUTHED: show only the login view ----
hideEl(wrap); // ensure app shell stays hidden while logged out
unhide(mainEl);
unhide(login);
if (login) login.style.display = '';
// …wire stuff…
applySiteConfig(window.__FR_SITE_CFG__ || {}, { phase: 'final' });
await revealAppAndHideOverlay();
const hb = document.querySelector('.header-buttons');
if (hb) hb.style.visibility = 'hidden';
// keep app cards inert while logged out (no layout poke)
['uploadCard', 'folderManagementCard'].forEach(id => { ['uploadCard', 'folderManagementCard'].forEach(id => {
const el = document.getElementById(id); const el = document.getElementById(id);
if (!el) return; if (!el) return;
el.style.display = 'none';
el.setAttribute('aria-hidden', 'true'); el.setAttribute('aria-hidden', 'true');
try { el.inert = true; } catch { } try { el.inert = true; } catch { }
}); });
bindLogin(); bindLogin();
wireCreateDropdown(); wireCreateDropdown();
keepCreateDropdownWired(); keepCreateDropdownWired();
wireModalEnterDefault(); wireModalEnterDefault();
showLoginTip('Please log in to continue'); showLoginTip('Please log in to continue');
}, { once: true }); // <— important if (overlay) overlay.style.display = 'none';
}, { once: true });
})(); })();

176
scripts/filerise-deploy.sh Normal file
View File

@@ -0,0 +1,176 @@
#!/usr/bin/env bash
# FileRise release deployer
# /usr/local/bin/filerise-deploy.sh and chmod +x /usr/local/bin/filerise-deploy.sh
# Usage:
# filerise-deploy.sh [vX.Y.Z|latest] [force]
# Examples:
# filerise-deploy.sh latest
# filerise-deploy.sh v1.7.4
# filerise-deploy.sh 1.7.4 force
set -euo pipefail
REPO="error311/FileRise"
DEST="/var/www"
OWNER="www-data:www-data" # change if your web user differs (e.g., apache:apache)
PHPFPM_SERVICES=(php8.4-fpm php8.3-fpm php8.2-fpm php8.1-fpm php8.0-fpm)
TAG_INPUT="${1:-latest}"
FORCE="${2:-}"
EXCLUDES=(
"--exclude=uploads"
"--exclude=uploads/**"
"--exclude=users"
"--exclude=users/**"
"--exclude=metadata"
"--exclude=metadata/**"
"--exclude=vendor"
"--exclude=vendor/**"
"--exclude=config/config.php"
"--exclude=.env"
)
die() { echo "ERROR: $*" >&2; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"; }
need curl
need unzip
need rsync
if [[ ! -d "$DEST" ]]; then
die "DEST '$DEST' does not exist. Create it or set DEST variable in script."
fi
create_dir() {
local sub="$1"
install -d -m 2775 -o "${OWNER%:*}" -g "${OWNER#*:}" "${DEST}/${sub}"
chmod g+s "${DEST}/${sub}" || true
}
ensure_writable_dirs() {
create_dir uploads
create_dir users
create_dir metadata
}
normalize_tag() {
local t="$1"
[[ "$t" =~ ^v ]] && echo "$t" || echo "v${t}"
}
# --- pick release tag ---
if [[ "$TAG_INPUT" == "latest" ]]; then
TAG="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
| grep -m1 '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')" || die "Could not resolve latest tag"
else
TAG="$(normalize_tag "$TAG_INPUT")"
fi
[[ -n "$TAG" ]] || die "Empty tag resolved."
# Skip if already on this version unless 'force'
if [[ -f "${DEST}/.filerise_version" ]] && grep -qx "${TAG}" "${DEST}/.filerise_version" && [[ "$FORCE" != "force" ]]; then
echo "FileRise ${TAG} already installed; nothing to do."
exit 0
fi
ensure_writable_dirs
ZIP_NAME="FileRise-${TAG}.zip"
ZIP_URL="https://github.com/${REPO}/releases/download/${TAG}/${ZIP_NAME}"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
echo "Downloading ${ZIP_URL}"
curl -fL --retry 3 -o "${WORKDIR}/${ZIP_NAME}" "${ZIP_URL}" || die "Download failed"
echo "Unzipping…"
unzip -q "${WORKDIR}/${ZIP_NAME}" -d "${WORKDIR}/unz" || die "Unzip failed"
# Determine source root inside the zip (prefer a directory with public/)
SRC_DIR=""
if [[ -d "${WORKDIR}/unz/public" ]]; then
SRC_DIR="${WORKDIR}/unz"
else
# find first top-level dir that contains public/
while IFS= read -r -d '' d; do
if [[ -d "$d/public" ]]; then SRC_DIR="$d"; break; fi
done < <(find "${WORKDIR}/unz" -mindepth 1 -maxdepth 1 -type d -print0)
# fallback: if nothing has public/, use unz root
SRC_DIR="${SRC_DIR:-${WORKDIR}/unz}"
fi
echo "Using source root: ${SRC_DIR}"
echo " - $(ls -1 ${SRC_DIR} | tr '\n' ' ')"
echo
# Sync to DEST while preserving data/secret bits
echo "Rsync → ${DEST}"
rsync -a --delete "${EXCLUDES[@]}" "${SRC_DIR}/" "${DEST}/"
# Stamp version file
echo "${TAG}" > "${DEST}/.filerise_version"
# Ensure writable dirs stay correct (even if rsync changed perms on parents)
chown -R "${OWNER}" "${DEST}/uploads" "${DEST}/users" "${DEST}/metadata"
chmod -R u+rwX,g+rwX "${DEST}/uploads" "${DEST}/users" "${DEST}/metadata"
find "${DEST}/uploads" "${DEST}/users" "${DEST}/metadata" -type d -exec chmod g+s {} + || true
# --- Composer dependencies (install only if needed) ---
install_composer_deps() {
need composer
echo "Installing Composer deps in ${DEST}"
# optimize + authoritative for prod
sudo -u "${OWNER%:*}" env COMPOSER_HOME="${DEST}" composer install \
--no-dev --prefer-dist --no-interaction --no-progress \
--optimize-autoloader --classmap-authoritative \
-d "${DEST}"
# record lock hash to trigger re-install only when lock changes
if [[ -f "${DEST}/composer.lock" ]]; then
sha256sum "${DEST}/composer.lock" | awk '{print $1}' > "${DEST}/.vendor_lock_hash"
fi
}
should_install_vendor() {
# no vendor dir → install
[[ ! -d "${DEST}/vendor" ]] && return 0
# empty vendor → install
[[ -z "$(ls -A "${DEST}/vendor" 2>/dev/null || true)" ]] && return 0
# if composer.lock exists and hash differs from last install → install
if [[ -f "${DEST}/composer.lock" ]]; then
local cur prev
cur="$(sha256sum "${DEST}/composer.lock" | awk '{print $1}')"
prev="$(cat "${DEST}/.vendor_lock_hash" 2>/dev/null || true)"
[[ "$cur" != "$prev" ]] && return 0
fi
return 1
}
if should_install_vendor; then
install_composer_deps
else
echo "Composer deps already up to date."
fi
# Reload PHP-FPM (clear opcache) if present
for svc in "${PHPFPM_SERVICES[@]}"; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
echo "Reloading ${svc}"
systemctl reload "$svc" || true
break
fi
done
# Reload Apache if present
if systemctl is-active --quiet apache2 2>/dev/null; then
echo "Reloading apache2 …"
systemctl reload apache2 || true
fi
# Quick sanity check: DocumentRoot should contain index
if [[ ! -f "${DEST}/public/index.html" && ! -f "${DEST}/public/index.php" ]]; then
echo "WARN: ${DEST}/public/index.(html|php) not found. Verify your release layout & DocumentRoot (${DEST}/public)."
fi
echo "Deployed FileRise ${TAG}${DEST}"