release(v1.7.5): CSP hardening, API-backed previews, flicker-free theming, cache tuning & deploy script (closes #50)
This commit is contained in:
38
CHANGELOG.md
38
CHANGELOG.md
@@ -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
|
||||||
|
|||||||
@@ -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 ---
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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; they’re 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>
|
||||||
@@ -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{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
})();
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 cache‐busted URL
|
// build a cache‐busted 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);
|
||||||
|
|||||||
@@ -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
176
scripts/filerise-deploy.sh
Normal 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}"
|
||||||
Reference in New Issue
Block a user