Compare commits

..

12 Commits

13 changed files with 1773 additions and 664 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
---
github: [error311]
ko_fi: error311

View File

@@ -1,5 +1,5 @@
---
name: Sync Changelog to Docker Repo
name: Bump version and sync Changelog to Docker Repo
on:
push:
@@ -10,35 +10,69 @@ permissions:
contents: write
jobs:
sync:
bump_and_sync:
runs-on: ubuntu-latest
steps:
- name: Checkout FileRise
uses: actions/checkout@v4
with:
path: file-rise
- uses: actions/checkout@v4
- name: Extract version from commit message
id: ver
run: |
MSG="${{ github.event.head_commit.message }}"
if [[ "$MSG" =~ release\((v[0-9]+\.[0-9]+\.[0-9]+)\) ]]; then
echo "version=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
echo "Found version: ${BASH_REMATCH[1]}"
else
echo "version=" >> $GITHUB_OUTPUT
echo "No release(vX.Y.Z) tag in commit message; skipping bump."
fi
- name: Update public/js/version.js
if: steps.ver.outputs.version != ''
run: |
cat > public/js/version.js <<'EOF'
// generated by CI
window.APP_VERSION = '${{ steps.ver.outputs.version }}';
EOF
- name: Commit version.js (if changed)
if: steps.ver.outputs.version != ''
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add public/js/version.js
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore: set APP_VERSION to ${{ steps.ver.outputs.version }}"
git push
fi
- name: Checkout filerise-docker
if: steps.ver.outputs.version != ''
uses: actions/checkout@v4
with:
repository: error311/filerise-docker
token: ${{ secrets.PAT_TOKEN }}
path: docker-repo
- name: Copy CHANGELOG.md
- name: Copy CHANGELOG.md and write VERSION
if: steps.ver.outputs.version != ''
run: |
cp file-rise/CHANGELOG.md docker-repo/CHANGELOG.md
cp CHANGELOG.md docker-repo/CHANGELOG.md
echo "${{ steps.ver.outputs.version }}" > docker-repo/VERSION
- name: Commit & push
- name: Commit & push to docker repo
if: steps.ver.outputs.version != ''
working-directory: docker-repo
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md
git add CHANGELOG.md VERSION
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore: sync CHANGELOG.md from FileRise"
git commit -m "chore: sync CHANGELOG.md and VERSION (${{ steps.ver.outputs.version }}) from FileRise"
git push origin main
fi

View File

@@ -1,5 +1,119 @@
# Changelog
## Changes 10/24/2025 (v1.6.5)
release(v1.6.5): fix PHP warning and upload-flag check in capabilities.php
- Fix undefined variable: use $disableUpload consistently
- Harden flag read: (bool)($perms['disableUpload'] ?? false)
- Prevents warning and ensures Upload capability is computed correctly
---
## Changes 10/24/2025 (v1.6.4)
release(v1.6.4): runtime version injection + CI bump/sync; caching tweaks
- Add public/js/version.js (default "dev") and load it before main.js.
- adminPanel.js: replace hard-coded string with `window.APP_VERSION || "dev"`.
- public/.htaccess: add no-cache for js/version.js
- GitHub Actions: replace sync job with “Bump version and sync Changelog to Docker Repo”.
- Parse commit msg `release(vX.Y.Z)` -> set step output `version`.
- Write `public/js/version.js` with `window.APP_VERSION = '<version>'`.
- Commit/push version.js if changed.
- Mirror CHANGELOG.md to filerise-docker and write a VERSION file with `<version>`.
- Guard all steps with `if: steps.ver.outputs.version != ''` to no-op on non-release commits.
This wires the UI version label to CI, keeps dev builds showing “dev”, and feeds the Docker repo with CHANGELOG + VERSION for builds.
---
## Changes 10/24/2025 (v1.6.3)
release(v1.6.3): drag/drop card persistence, admin UX fixes, and docs (closes #58)
Drag & Drop - Upload/Folder Management Cards layout
- Persist panel locations across refresh; snapshot + restore when collapsing/expanding.
- Unified “zones” toggle; header-icon mode no longer loses card state.
- Responsive: auto-move sidebar cards to top on small screens; restore on resize.
- Better top-zone placeholder/cleanup during drag; tighter header modal sizing.
- Safer order saving + deterministic placement for upload/folder cards.
Admin Panel Folder Access
- Fix: newly created folders now appear without a full page refresh (cache-busted `getFolderList`).
- Show admin users in the list with full access pre-applied and inputs disabled (read-only).
- Skip sending updates for admins when saving grants.
- “Folder” column now has its own horizontal scrollbar so long names / “Inherited from …” are never cut off.
Admin Panel User Permissions (flags)
- Show admins (marked as Admin) with all switches disabled; exclude from save payload.
- Clarified helper text (account-level vs per-folder).
UI/Styling
- Added `.folder-cell` scroller in ACL table; improved dark-mode scrollbar/thumb.
Docs
- README edits:
- Clarified PUID/PGID mapping and host/NAS ownership requirements for mounted volumes.
- Environment variables section added
- CHOWN_ON_START additional details
- Admin details
- Upgrade section added
- 💖 Sponsor FileRise section added
---
## Changes 10/23/2025 (v1.6.2)
feat(i18n,auth): add Simplified Chinese (zh-CN) and expose in User Panel
- Add zh-CN locale to i18n.js with full key set.
- Introduce chinese_simplified label key across locales.
- Added some missing labels
- Update language selector mapping to include zh-CN (English/Spanish/French/German/简体中文).
- Wire zh-CN into Auth/User Panel (authModals) language dropdown.
- Fallback-safe rendering for language names when a key is missing.
ui: fix “Change Password” button sizing in User Panel
- Keep consistent padding and font size for cleaner layout
---
## Changes 10/23/2025 (v1.6.1)
feat(ui): unified zone toggle + polished interactions for sidebar/top cards
- Add floating toggle button styling (hover lift, press, focus ring, ripple)
for #zonesToggleFloating and #sidebarToggleFloating (CSS).
- Ensure icons are visible and centered; enforce consistent sizing/color.
- Introduce unified “zones collapsed” state persisted via `localStorage.zonesCollapsed`.
- Update dragAndDrop.js to:
- manage a single floating toggle for both Sidebar and Top Zone
- keep toggle visible when cards are in Top Zone; hide only when both cards are in Header
- rotate icon 90° when both cards are in Top Zone and panels are open
- respect collapsed state during DnD flows and on load
- preserve original DnD behaviors and saved orders (sidebar/header)
- Minor layout/visibility fixes during drag (clear temp heights; honor collapsed).
Notes:
- No breaking API changes; existing `sidebarOrder` / `headerOrder` continue to work.
- New key: `zonesCollapsed` (string '0'/'1') controls visibility of Sidebar + Top Zone.
UX:
- Floating toggle feels more “material”: subtle hover elevation, press feedback,
focus ring, and click ripple to restore the prior interactive feel.
- Icons remain legible on white (explicit color set), centered in the circular button.
---
## Changes 10/22/2025 (v1.6.0)
feat(acl): granular per-folder permissions + stricter gates; WebDAV & UI aligned

View File

@@ -7,6 +7,8 @@
[![Demo](https://img.shields.io/badge/demo-live-brightgreen)](https://demo.filerise.net)
[![Release](https://img.shields.io/github/v/release/error311/FileRise?include_prereleases&sort=semver)](https://github.com/error311/FileRise/releases)
[![License](https://img.shields.io/github/license/error311/FileRise)](LICENSE)
[![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-❤-red)](https://github.com/sponsors/error311)
[![Support on Ko-fi](https://img.shields.io/badge/Ko--fi-Buy%20me%20a%20coffee-orange)](https://ko-fi.com/error311)
**Quick links:** [Demo](#live-demo) • [Install](#installation--setup) • [Docker](#1-running-with-docker-recommended) • [Unraid](#unraid) • [WebDAV](#quick-start-mount-via-webdav) • [FAQ](#faq--troubleshooting)
@@ -80,7 +82,7 @@ With drag-and-drop uploads, in-browser editing, secure user logins (SSO & TOTP 2
- 🎨 **Responsive UI (Dark/Light Mode):** Modern, mobile-friendly design with persistent preferences (theme, layout, last folder, etc.).
- 🌐 **Internationalization:** English, Spanish, French, and German available. Community translations welcome.
- 🌐 **Internationalization:** English, Spanish, French, German & Simplified Chinese available. Community translations welcome.
- ⚙️ **Lightweight & Self-Contained:** Runs on PHP 8.3+, no external DB required. Single-folder or Docker deployment with minimal footprint, optimized for Unraid and self-hosting.
@@ -103,6 +105,22 @@ Deploy FileRise using the **Docker image** (quickest) or a **manual install** on
---
### Environment variables
| Variable | Default | Purpose |
|---|---|---|
| `TIMEZONE` | `UTC` | PHP/app timezone. |
| `DATE_TIME_FORMAT` | `m/d/y h:iA` | Display format used in UI. |
| `TOTAL_UPLOAD_SIZE` | `5G` | Max combined upload per request (resumable). |
| `SECURE` | `false` | Set `true` if served behind HTTPS proxy (affects link generation). |
| `PERSISTENT_TOKENS_KEY` | *(required)* | Secret for “Remember Me” tokens. Change from the example! |
| `PUID` / `PGID` | `1000` / `1000` | Map `www-data` to host uid:gid (Unraid: often `99:100`). |
| `CHOWN_ON_START` | `true` | First run: try to chown mounted dirs to PUID:PGID. |
| `SCAN_ON_START` | `true` | Reindex files added outside UI at boot. |
| `SHARE_URL` | *(blank)* | Override base URL for share links; blank = auto-detect. |
---
### 1) Running with Docker (Recommended)
#### Pull the image
@@ -133,6 +151,8 @@ docker run -d \
error311/filerise-docker:latest
```
The app runs as www-data mapped to PUID/PGID. Ensure your mounted uploads/, users/, metadata/ are owned by PUID:PGID (e.g., chown -R 1000:1000 …), or set PUID/PGID to match existing host ownership (e.g., 99:100 on Unraid). On NAS/NFS, apply the ownership change on the host/NAS.
This starts FileRise on port **8080** → visit `http://your-server-ip:8080`.
**Notes**
@@ -183,6 +203,8 @@ services:
Access at `http://localhost:8080` (or your servers IP).
The example sets a custom `PERSISTENT_TOKENS_KEY`—change it to a strong random string.
-`CHOWN_ON_START=true` attempts to align ownership **inside the container**; if the host/NAS disallows changes, set the correct UID/GID on the host.”
**First-time Setup**
On first launch, if no users exist, youll be prompted to create an **Admin account**. Then use **User Management** to add more users.
@@ -247,6 +269,13 @@ Browse to your FileRise URL; youll be prompted to create the Admin user on fi
---
### 3) Admins
> **Admins in ACL UI**
> Admin accounts appear in the Folder Access and User Permissions modals as **read-only** with full access implied. This is by design—admins always have full control and are excluded from save payloads.
---
## Unraid
- Install from **Community Apps** → search **FileRise**.
@@ -256,6 +285,16 @@ Browse to your FileRise URL; youll be prompted to create the Admin user on fi
---
## Upgrade
```bash
docker pull error311/filerise-docker:latest
docker stop filerise && docker rm filerise
# re-run with the same -v and -e flags you used originally
```
---
## Quick-start: Mount via WebDAV
Once FileRise is running, enable WebDAV in the admin panel.
@@ -336,6 +375,17 @@ If you like FileRise, a ⭐ star on GitHub is much appreciated!
---
## 💖 Sponsor FileRise
If FileRise saves you time (or sparks joy 😄), please consider supporting ongoing development:
- ❤️ [**GitHub Sponsors:**](https://github.com/sponsors/error311) recurring or one-time - helps fund new features and docs.
- ☕ [**Ko-fi:**](https://ko-fi.com/error311) buy me a coffee.
Every bit helps me keep FileRise fast, polished, and well-maintained. Thank you!
---
## Community and Support
- **Reddit:** [r/selfhosted: FileRise Discussion](https://www.reddit.com/r/selfhosted/comments/1kfxo9y/filerise_v131_major_updates_sneak_peek_at_whats/) (Announcement and user feedback thread).

View File

@@ -50,6 +50,12 @@ RewriteEngine On
<FilesMatch "\.(js|css)$">
Header set Cache-Control "public, max-age=3600, must-revalidate"
</FilesMatch>
# version.js should always revalidate (it changes on releases)
<FilesMatch "^js/version\.js$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
</FilesMatch>
</IfModule>
# -----------------------------

View File

@@ -153,7 +153,7 @@ if ($folder !== 'root') {
$perms = loadPermsFor($username);
$isAdmin = ACL::isAdmin($perms);
$readOnly = !empty($perms['readOnly']);
$disableUp = !empty($perms['disableUpload']);
$disableUpload = (bool)($perms['disableUpload'] ?? false);
$inScope = inUserFolderScope($folder, $username, $perms, $isAdmin);
// --- ACL base abilities ---
@@ -213,7 +213,7 @@ echo json_encode([
'flags' => [
//'folderOnly' => !empty($perms['folderOnly']) || !empty($perms['userFolderOnly']) || !empty($perms['UserFolderOnly']),
'readOnly' => $readOnly,
'disableUpload' => $disableUp,
'disableUpload' => $disableUpload,
],
'owner' => $owner,

View File

@@ -2036,10 +2036,9 @@ body.dark-mode .admin-panel-content label {
}
#openChangePasswordModalBtn {
width: auto;
padding: 5px 10px;
width: max-content;
padding: 6px 12px;
font-size: 14px;
margin-right: 300px;
}
#changePasswordModal {
@@ -2309,3 +2308,68 @@ body.dark-mode .user-dropdown .user-menu .item:hover {
:root { --perm-caret: #444; } /* light */
body.dark-mode { --perm-caret: #ccc; } /* dark */
#zonesToggleFloating,
#sidebarToggleFloating {
transition:
transform 160ms cubic-bezier(.2,.0,.2,1),
box-shadow 160ms cubic-bezier(.2,.0,.2,1),
border-color 160ms cubic-bezier(.2,.0,.2,1),
background-color 160ms cubic-bezier(.2,.0,.2,1);
}
#zonesToggleFloating .material-icons,
#zonesToggleFloating .material-icons-outlined,
#sidebarToggleFloating .material-icons,
#sidebarToggleFloating .material-icons-outlined {
color: #333 !important;
font-size: 22px;
line-height: 1;
display: block;
}
#zonesToggleFloating:hover,
#sidebarToggleFloating:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(0,0,0,.14);
border-color: #cfcfcf;
}
#zonesToggleFloating:active,
#sidebarToggleFloating:active {
transform: translateY(0) scale(.96);
box-shadow: 0 3px 8px rgba(0,0,0,.12);
}
#zonesToggleFloating:focus-visible,
#sidebarToggleFloating:focus-visible {
outline: none;
box-shadow:
0 6px 16px rgba(0,0,0,.14),
0 0 0 3px rgba(25,118,210,.25); /* soft brandy ring */
}
#zonesToggleFloating::after,
#sidebarToggleFloating::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(circle, rgba(0,0,0,.12) 0%, rgba(0,0,0,0) 60%);
transform: scale(0);
opacity: 0;
transition: transform 300ms ease, opacity 450ms ease;
pointer-events: none;
}
#zonesToggleFloating:active::after,
#sidebarToggleFloating:active::after {
transform: scale(1.4);
opacity: 1;
}
#zonesToggleFloating.is-collapsed,
#sidebarToggleFloating.is-collapsed {
background: #fafafa;
border-color: #e2e2e2;
}

View File

@@ -563,6 +563,7 @@
</div>
</div>
</div>
<script src="js/version.js"></script>
<script type="module" src="js/main.js"></script>
</body>

View File

@@ -4,10 +4,19 @@ import { loadAdminConfigFunc } from './auth.js';
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
import { sendRequest } from './networkUtils.js';
const version = "v1.6.0";
const version = window.APP_VERSION || "dev";
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
function buildFullGrantsForAllFolders(folders) {
const allTrue = {
view:true, viewOwn:false, manage:true, create:true, upload:true, edit:true,
rename:true, copy:true, move:true, delete:true, extract:true,
shareFile:true, shareFolder:true, share:true
};
return folders.reduce((acc, f) => { acc[f] = { ...allTrue }; return acc; }, {});
}
/* === BEGIN: Folder Access helpers (merged + improved) === */
function qs(scope, sel){ return (scope||document).querySelector(sel); }
function qsa(scope, sel){ return Array.from((scope||document).querySelectorAll(sel)); }
@@ -194,6 +203,25 @@ async function safeJson(res) {
@media (max-width: 900px) {
.folder-access-list { --col-perm: 72px; --col-folder-min: 240px; }
}
/* Folder cell: horizontal-only scroll */
.folder-cell{
overflow-x:auto;
overflow-y:hidden;
white-space:nowrap;
-webkit-overflow-scrolling:touch;
}
/* nicer thin scrollbar (supported browsers) */
.folder-cell::-webkit-scrollbar{ height:8px; }
.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); }
/* Badge now doesn't clip; let the wrapper handle scroll */
.folder-badge{
display:inline-flex; align-items:center; gap:6px;
font-weight:600;
min-width:0; /* allow child to be as wide as needed inside scroller */
}
`;
document.head.appendChild(style);
})();
@@ -617,21 +645,29 @@ export async function closeAdminPanel() {
New: Folder Access (ACL) UI
=========================== */
let __allFoldersCache = null; // array of folder strings
async function getAllFolders() {
if (__allFoldersCache) return __allFoldersCache.slice();
const res = await fetch('/api/folder/getFolderList.php', { credentials: 'include' });
let __allFoldersCache = null;
async function getAllFolders(force = false) {
if (!force && __allFoldersCache) return __allFoldersCache.slice();
const res = await fetch('/api/folder/getFolderList.php?ts=' + Date.now(), {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-store' }
});
const data = await safeJson(res).catch(() => []);
const list = Array.isArray(data)
? data.map(x => (typeof x === 'string' ? x : x.folder)).filter(Boolean)
: [];
const hidden = new Set(["profile_pics", "trash"]);
const hidden = new Set(['profile_pics', 'trash']);
const cleaned = list
.filter(f => f && !hidden.has(f.toLowerCase()))
.sort((a, b) => (a === 'root' ? -1 : b === 'root' ? 1 : a.localeCompare(b)));
__allFoldersCache = cleaned;
return cleaned.slice();
}
}
async function getUserGrants(username) {
const res = await fetch(`/api/admin/acl/getGrants.php?user=${encodeURIComponent(username)}`, {
@@ -674,7 +710,9 @@ function renderFolderGrantsUI(username, container, folders, grants) {
const headerHtml = `
<div class="folder-access-header">
<div title="${tf('folder_help', 'Folder path within FileRise')}">${tf('folder', 'Folder')}</div>
<div class="folder-cell" title="${tf('folder_help','Folder path within FileRise')}">
${tf('folder','Folder')}
</div>
<div class="perm-col" title="${tf('view_all_help', 'See all files in this folder (everyones files)')}">${tf('view_all', 'View (all)')}</div>
<div class="perm-col" title="${tf('view_own_help', 'See only files you uploaded in this folder')}">${tf('view_own', 'View (own)')}</div>
<div class="perm-col" title="${tf('write_help', 'Meta: toggles all write operations (below) on/off for this row')}">${tf('write_full', 'Write')}</div>
@@ -698,7 +736,13 @@ function renderFolderGrantsUI(username, container, folders, grants) {
const shareFolderDisabled = !g.view;
return `
<div class="folder-access-row" data-folder="${folder}">
<div class="folder-badge"><i class="material-icons" style="font-size:18px;">folder</i>${name}<span class="inherited-tag" style="display:none;"></span></div>
<div class="folder-cell">
<div class="folder-badge">
<i class="material-icons" style="font-size:18px;">folder</i>
${name}
<span class="inherited-tag" style="display:none;"></span>
</div>
</div>
<div class="perm-col"><input type="checkbox" data-cap="view" ${g.view ? 'checked' : ''}></div>
<div class="perm-col"><input type="checkbox" data-cap="viewOwn" ${g.viewOwn ? 'checked' : ''}></div>
<div class="perm-col"><input type="checkbox" data-cap="write" ${writeMetaChecked ? 'checked' : ''}></div>
@@ -999,15 +1043,16 @@ export function openUserPermissionsModal() {
});
document.getElementById("saveUserPermissionsBtn").addEventListener("click", async () => {
const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
const changes = [];
rows.forEach(row => {
const changes = [];
rows.forEach(row => {
if (row.getAttribute("data-admin") === "1") return; // skip admins
const username = String(row.getAttribute("data-username") || "").trim();
if (!username) return;
const grantsBox = row.querySelector(".folder-grants-box");
if (!grantsBox || grantsBox.getAttribute('data-loaded') !== '1') return;
const grants = collectGrantsFrom(grantsBox);
changes.push({ user: username, grants });
});
});
try {
if (changes.length === 0) { showToast(tf("nothing_to_save", "Nothing to save")); return; }
await sendRequest("/api/admin/acl/saveGrants.php", "POST",
@@ -1053,14 +1098,17 @@ async function fetchAllUserFlags() {
function flagRow(u, flags) {
const f = flags[u.username] || {};
const isAdmin = String(u.role) === "1" || u.username.toLowerCase() === "admin";
if (isAdmin) return "";
const disabledAttr = isAdmin ? "disabled data-admin='1' title='Admin: full access'" : "";
const note = isAdmin ? " <span class='muted'>(Admin)</span>" : "";
return `
<tr data-username="${u.username}">
<td><strong>${u.username}</strong></td>
<td style="text-align:center;"><input type="checkbox" data-flag="readOnly" ${f.readOnly ? "checked" : ""}></td>
<td style="text-align:center;"><input type="checkbox" data-flag="disableUpload" ${f.disableUpload ? "checked" : ""}></td>
<td style="text-align:center;"><input type="checkbox" data-flag="canShare" ${f.canShare ? "checked" : ""}></td>
<td style="text-align:center;"><input type="checkbox" data-flag="bypassOwnership" ${f.bypassOwnership ? "checked" : ""}></td>
<tr data-username="${u.username}" ${isAdmin ? "data-admin='1'" : ""}>
<td><strong>${u.username}</strong>${note}</td>
<td style="text-align:center;"><input type="checkbox" data-flag="readOnly" ${f.readOnly ? "checked" : ""} ${disabledAttr}></td>
<td style="text-align:center;"><input type="checkbox" data-flag="disableUpload" ${f.disableUpload ? "checked" : ""} ${disabledAttr}></td>
<td style="text-align:center;"><input type="checkbox" data-flag="canShare" ${f.canShare ? "checked" : ""} ${disabledAttr}></td>
<td style="text-align:center;"><input type="checkbox" data-flag="bypassOwnership" ${f.bypassOwnership ? "checked" : ""} ${disabledAttr}></td>
</tr>
`;
}
@@ -1092,7 +1140,7 @@ export async function openUserFlagsModal() {
<h3>${tf("user_permissions", "User Permissions")}</h3>
<p class="muted" style="margin-top:-6px;">
${tf("user_flags_help", "Account-level switches. These are NOT per-folder grants.")}
${tf("user_flags_help", "Non Admin User Account-level switches. These are NOT per-folder grants.")}
</p>
<div id="userFlagsBody"
@@ -1141,7 +1189,7 @@ async function loadUserFlagsList() {
<th>${t("read_only")}</th>
<th>${t("disable_upload")}</th>
<th>${t("can_share")}</th>
<th>bypassOwnership</th>
<th>${t("bypass_ownership")}</th>
</tr>
</thead>
<tbody>${rows || `<tr><td colspan="6">${t("no_users_found")}</td></tr>`}</tbody>
@@ -1158,6 +1206,7 @@ async function saveUserFlags() {
const rows = body?.querySelectorAll("tbody tr[data-username]") || [];
const permissions = [];
rows.forEach(tr => {
if (tr.getAttribute("data-admin") === "1") return; // don't send admin updates
const username = tr.getAttribute("data-username");
const get = k => tr.querySelector(`input[data-flag="${k}"]`).checked;
permissions.push({
@@ -1201,15 +1250,16 @@ async function loadUserPermissionsList() {
return;
}
const folders = await getAllFolders();
const folders = await getAllFolders(true);
listContainer.innerHTML = "";
users.forEach(user => {
if ((user.role && String(user.role) === "1") || String(user.username).toLowerCase() === "admin") return;
users.forEach(user => {
const isAdmin = (user.role && String(user.role) === "1") || String(user.username).toLowerCase() === "admin";
const row = document.createElement("div");
row.classList.add("user-permission-row");
row.setAttribute("data-username", user.username);
if (isAdmin) row.setAttribute("data-admin", "1"); // mark admins
row.style.padding = "6px 0";
row.innerHTML = `
@@ -1217,7 +1267,8 @@ async function loadUserPermissionsList() {
style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:6px 8px;border-radius:6px;">
<span class="perm-caret" style="display:inline-block; transform: rotate(-90deg); transition: transform 120ms ease;">▸</span>
<strong>${user.username}</strong>
<span class="muted" style="margin-left:auto;">${tf('click_to_edit', 'Click to edit')}</span>
${isAdmin ? `<span class="muted" style="margin-left:auto;">Admin (full access)</span>`
: `<span class="muted" style="margin-left:auto;">${tf('click_to_edit', 'Click to edit')}</span>`}
</div>
<div class="user-perm-details" style="display:none; margin:8px 0 12px;">
<div class="folder-grants-box" data-loaded="0"></div>
@@ -1232,8 +1283,18 @@ async function loadUserPermissionsList() {
async function ensureLoaded() {
if (grantsBox.dataset.loaded === "1") return;
try {
const grants = await getUserGrants(user.username);
renderFolderGrantsUI(user.username, grantsBox, ["root", ...folders.filter(f => f !== "root")], grants);
let grants;
if (isAdmin) {
// synthesize full access
const ordered = ["root", ...folders.filter(f => f !== "root")];
grants = buildFullGrantsForAllFolders(ordered);
renderFolderGrantsUI(user.username, grantsBox, ordered, grants);
// disable all inputs
grantsBox.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.disabled = true);
} else {
const userGrants = await getUserGrants(user.username);
renderFolderGrantsUI(user.username, grantsBox, ["root", ...folders.filter(f => f !== "root")], userGrants);
}
grantsBox.dataset.loaded = "1";
} catch (e) {
console.error(e);
@@ -1255,7 +1316,7 @@ async function loadUserPermissionsList() {
});
listContainer.appendChild(row);
});
});
} catch (err) {
console.error(err);
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";

View File

@@ -328,10 +328,19 @@ export async function openUserPanel() {
const langSel = document.createElement('select');
langSel.id = 'languageSelector';
langSel.className = 'form-select';
['en', 'es', 'fr', 'de'].forEach(code => {
const languages = [
{ code: 'en', labelKey: 'english', fallback: 'English' },
{ code: 'es', labelKey: 'spanish', fallback: 'Español' },
{ code: 'fr', labelKey: 'french', fallback: 'Français' },
{ code: 'de', labelKey: 'german', fallback: 'Deutsch' },
{ code: 'zh-CN', labelKey: 'chinese_simplified', fallback: '简体中文' },
];
languages.forEach(({ code, labelKey, fallback }) => {
const opt = document.createElement('option');
opt.value = code;
opt.textContent = t(code === 'en' ? 'english' : code === 'es' ? 'spanish' : code === 'fr' ? 'french' : 'german');
// use i18n if available, otherwise fallback
opt.textContent = (typeof t === 'function' ? t(labelKey) : '') || fallback;
langSel.appendChild(opt);
});
langSel.value = localStorage.getItem('language') || 'en';

File diff suppressed because it is too large Load Diff

View File

@@ -216,6 +216,7 @@ const translations = {
"spanish": "Spanish",
"french": "French",
"german": "German",
"chinese_simplified": "Chinese (Simplified)",
"use_totp_code_instead": "Use TOTP Code instead",
"submit_recovery_code": "Submit Recovery Code",
"please_enter_recovery_code": "Please enter your recovery code.",
@@ -275,7 +276,13 @@ const translations = {
"newfile_placeholder": "New file name",
"file_created_successfully": "File created successfully!",
"error_creating_file": "Error creating file",
"file_created": "File created successfully!"
"file_created": "File created successfully!",
"no_access_to_resource": "You do not have access to this resource.",
"can_share": "Can Share",
"bypass_ownership": "Bypass Ownership",
"error_loading_user_grants": "Error loading user grants",
"click_to_edit": "Click to edit",
"folder_access": "Folder Access"
},
es: {
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
@@ -458,6 +465,7 @@ const translations = {
"spanish": "Español",
"french": "Francés",
"german": "Alemán",
"chinese_simplified": "Chino (simplificado)",
"use_totp_code_instead": "Usar código TOTP en su lugar",
"submit_recovery_code": "Enviar código de recuperación",
"please_enter_recovery_code": "Por favor, ingrese su código de recuperación.",
@@ -686,6 +694,7 @@ const translations = {
"spanish": "Espagnol",
"french": "Français",
"german": "Allemand",
"chinese_simplified": "Chinois (simplifié)",
"use_totp_code_instead": "Utiliser le code TOTP à la place",
"submit_recovery_code": "Soumettre le code de récupération",
"please_enter_recovery_code": "Veuillez entrer votre code de récupération.",
@@ -923,6 +932,7 @@ const translations = {
"spanish": "Spanisch",
"french": "Französisch",
"german": "Deutsch",
"chinese_simplified": "Chinesisch (vereinfacht)",
"use_totp_code_instead": "Stattdessen TOTP-Code verwenden",
"submit_recovery_code": "Wiederherstellungscode absenden",
"please_enter_recovery_code": "Bitte geben Sie Ihren Wiederherstellungscode ein.",
@@ -972,7 +982,275 @@ const translations = {
"show": "Zeige",
"items_per_page": "elemente pro seite",
"columns": "Spalten"
},
"zh-CN": {
"please_log_in_to_continue": "请登录以继续。",
"no_files_selected": "未选择文件。",
"confirm_delete_files": "确定要删除所选的 {count} 个文件吗?",
"element_not_found": "未找到 ID 为 \"{id}\" 的元素。",
"search_placeholder": "搜索文件、标签和上传者…",
"search_placeholder_advanced": "高级搜索:文件、标签、上传者和内容…",
"basic_search_tooltip": "基础搜索:按文件名、标签和上传者搜索。",
"advanced_search_tooltip": "高级搜索:包括文件内容、文件名、标签和上传者。",
"file_name": "文件名",
"date_modified": "修改日期",
"upload_date": "上传日期",
"file_size": "文件大小",
"uploader": "上传者",
"enter_totp_code": "输入 TOTP 验证码",
"use_recovery_code_instead": "改用恢复代码",
"enter_recovery_code": "输入恢复代码",
"editing": "正在编辑",
"decrease_font": "A-",
"increase_font": "A+",
"save": "保存",
"close": "关闭",
"no_files_found": "未找到文件。",
"switch_to_table_view": "切换到表格视图",
"switch_to_gallery_view": "切换到图库视图",
"share_file": "分享文件",
"set_expiration": "设置到期时间:",
"password_optional": "密码(可选):",
"generate_share_link": "生成分享链接",
"shareable_link": "可分享链接:",
"copy_link": "复制链接",
"tag_file": "标记文件",
"tag_name": "标签名称:",
"tag_color": "标签颜色:",
"save_tag": "保存标签",
"light_mode": "浅色模式",
"dark_mode": "深色模式",
"upload_instruction": "将文件/文件夹拖到此处,或点击“选择文件”",
"no_files_selected_default": "未选择文件",
"choose_files": "选择文件",
"delete_selected": "删除所选",
"copy_selected": "复制所选",
"move_selected": "移动所选",
"tag_selected": "标记所选",
"download_zip": "下载 ZIP",
"extract_zip": "解压 ZIP",
"preview": "预览",
"edit": "编辑",
"rename": "重命名",
"trash_empty": "回收站为空。",
"no_trash_selected": "未选择要还原的回收站项目。",
"title": "FileRise",
"header_title": "FileRise",
"header_title_text": "标题文本",
"logout": "退出登录",
"change_password": "更改密码",
"restore_text": "还原或",
"delete_text": "删除回收站项目",
"restore_selected": "还原所选",
"restore_all": "全部还原",
"delete_selected_trash": "删除所选",
"delete_all": "全部删除",
"upload_header": "上传文件/文件夹",
"folder_navigation": "文件夹导航与管理",
"create_folder": "创建文件夹",
"create_folder_title": "创建文件夹",
"enter_folder_name": "输入文件夹名称",
"cancel": "取消",
"create": "创建",
"rename_folder": "重命名文件夹",
"rename_folder_title": "重命名文件夹",
"rename_folder_placeholder": "输入新的文件夹名称",
"delete_folder": "删除文件夹",
"delete_folder_title": "删除文件夹",
"delete_folder_message": "确定要删除此文件夹吗?",
"folder_help": "文件夹帮助",
"folder_help_item_1": "点击文件夹以查看其中的文件。",
"folder_help_item_2": "使用 [-] 折叠,使用 [+] 展开文件夹。",
"folder_help_item_3": "选择一个文件夹并点击“创建文件夹”以添加子文件夹。",
"folder_help_item_4": "要重命名或删除文件夹,请选择后点击相应按钮。",
"actions": "操作",
"file_list_title": "文件列表(根目录)",
"files_in": "文件位于",
"delete_files": "删除文件",
"delete_selected_files_title": "删除所选文件",
"delete_files_message": "确定要删除所选文件吗?",
"copy_files": "复制文件",
"copy_files_title": "复制所选文件",
"copy_files_message": "选择目标文件夹以复制所选文件:",
"move_files": "移动文件",
"move_files_title": "移动所选文件",
"move_files_message": "选择目标文件夹以移动所选文件:",
"move": "移动",
"extract_zip_button": "解压 ZIP",
"download_zip_title": "将所选文件打包为 ZIP 下载",
"download_zip_prompt": "输入 ZIP 文件名:",
"zip_placeholder": "files.zip",
"share": "分享",
"total_files": "文件总数",
"total_size": "总大小",
"prev": "上一页",
"next": "下一页",
"page": "第",
"of": "页,共",
"login": "登录",
"remember_me": "记住我",
"login_oidc": "使用 OIDC 登录",
"basic_http_login": "使用基本 HTTP 登录",
"change_password_title": "更改密码",
"old_password": "旧密码",
"new_password": "新密码",
"confirm_new_password": "确认新密码",
"create_new_user_title": "创建新用户",
"username": "用户名:",
"password": "密码:",
"enter_password": "密码",
"preparing_download": "正在准备下载…",
"download_file": "下载文件",
"confirm_or_change_filename": "确认或修改下载文件名:",
"filename": "文件名",
"download": "下载",
"grant_admin": "授予管理员权限",
"save_user": "保存用户",
"remove_user_title": "删除用户",
"select_user_remove": "选择要删除的用户:",
"delete_user": "删除用户",
"rename_file_title": "重命名文件",
"rename_file_placeholder": "输入新的文件名",
"share_folder": "分享文件夹",
"allow_uploads": "允许上传",
"share_link_generated": "已生成分享链接",
"error_generating_share_link": "生成分享链接时出错",
"custom": "自定义",
"duration": "持续时间",
"seconds": "秒",
"minutes": "分钟",
"hours": "小时",
"days": "天",
"custom_duration_warning": "⚠️ 使用较长的到期时间可能存在安全风险,请谨慎使用。",
"folder_share": "分享文件夹",
"yes": "是",
"no": "否",
"unsaved_changes_confirm": "您有未保存的更改,确定要关闭而不保存吗?",
"delete": "删除",
"upload": "上传",
"copy": "复制",
"extract": "解压",
"user": "用户:",
"unknown_error": "未知错误",
"link_copied": "链接已复制到剪贴板",
"weeks": "周",
"months": "月",
"dark_mode_toggle": "深色模式",
"light_mode_toggle": "浅色模式",
"switch_to_light_mode": "切换到浅色模式",
"switch_to_dark_mode": "切换到深色模式",
"header_settings": "标题设置",
"shared_max_upload_size_bytes_title": "共享最大上传大小",
"shared_max_upload_size_bytes": "共享最大上传大小(字节)",
"max_bytes_shared_uploads_note": "请输入共享文件夹上传的最大允许字节数",
"manage_shared_links": "管理分享链接",
"folder_shares": "文件夹分享",
"file_shares": "文件分享",
"loading": "正在加载…",
"error_loading_share_links": "加载分享链接时出错",
"share_deleted_successfully": "分享已成功删除",
"error_deleting_share": "删除分享时出错",
"password_protected": "受密码保护",
"no_shared_links_available": "暂无可用的分享链接",
"admin_panel": "管理员面板",
"user_panel": "用户面板",
"user_settings": "用户设置",
"save_profile_picture": "保存头像",
"please_select_picture": "请选择图片",
"profile_picture_updated": "头像已更新",
"error_updating_picture": "更新头像时出错",
"trash_restore_delete": "回收站恢复/删除",
"totp_settings": "TOTP 设置",
"enable_totp": "启用 TOTP",
"language": "语言",
"select_language": "选择语言",
"english": "英语",
"spanish": "西班牙语",
"french": "法语",
"german": "德语",
"chinese_simplified": "简体中文",
"use_totp_code_instead": "改用 TOTP 验证码",
"submit_recovery_code": "提交恢复代码",
"please_enter_recovery_code": "请输入您的恢复代码。",
"recovery_code_verification_failed": "恢复代码验证失败",
"error_verifying_recovery_code": "验证恢复代码时出错",
"totp_verification_failed": "TOTP 验证失败",
"error_verifying_totp_code": "验证 TOTP 代码时出错",
"totp_setup": "TOTP 设置",
"scan_qr_code": "请使用验证器应用扫描此二维码。",
"enter_totp_confirmation": "输入应用生成的 6 位验证码以确认设置:",
"confirm": "确认",
"please_enter_valid_code": "请输入有效的 6 位验证码。",
"totp_enabled_successfully": "TOTP 启用成功。",
"error_generating_recovery_code": "生成恢复代码时出错",
"error_loading_qr_code": "加载二维码时出错。",
"error_disabling_totp_setting": "禁用 TOTP 设置时出错",
"user_management": "用户管理",
"add_user": "添加用户",
"remove_user": "删除用户",
"user_permissions": "用户权限",
"oidc_configuration": "OIDC 配置",
"oidc_provider_url": "OIDC 提供者 URL",
"oidc_client_id": "OIDC 客户端 ID",
"oidc_client_secret": "OIDC 客户端密钥",
"oidc_redirect_uri": "OIDC 重定向 URI",
"global_totp_settings": "全局 TOTP 设置",
"global_otpauth_url": "全局 OTPAuth URL",
"login_options": "登录选项",
"disable_login_form": "禁用登录表单",
"disable_basic_http_auth": "禁用基本 HTTP 认证",
"disable_oidc_login": "禁用 OIDC 登录",
"save_settings": "保存设置",
"at_least_one_login_method": "至少保留一种登录方式。",
"settings_updated_successfully": "设置已成功更新。",
"error_updating_settings": "更新设置时出错",
"user_permissions_updated_successfully": "用户权限已成功更新。",
"error_updating_permissions": "更新权限时出错",
"no_users_found": "未找到用户。",
"user_folder_only": "仅限用户文件夹",
"read_only": "只读",
"disable_upload": "禁用上传",
"error_loading_users": "加载用户时出错",
"save_permissions": "保存权限",
"your_recovery_code": "您的恢复代码",
"please_save_recovery_code": "请妥善保存此代码。此代码仅显示一次且只能使用一次。",
"ok": "确定",
"show": "显示",
"items_per_page": "每页项目数",
"columns": "列",
"row_height": "行高",
"api_docs": "API 文档",
"show_folders_above_files": "在文件上方显示文件夹",
"display": "显示",
"create_file": "创建文件",
"create_new_file": "创建新文件",
"enter_file_name": "输入文件名",
"newfile_placeholder": "新文件名",
"file_created_successfully": "文件创建成功!",
"error_creating_file": "创建文件时出错",
"file_created": "文件创建成功!",
"no_access_to_resource": "您无权访问此资源。",
"can_share": "可分享",
"bypass_ownership": "绕过所有权限制",
"error_loading_user_grants": "加载用户授权时出错",
"click_to_edit": "点击编辑",
"folder_access": "文件夹访问"
}
};
let currentLocale = 'en';

2
public/js/version.js Normal file
View File

@@ -0,0 +1,2 @@
// generated by CI
window.APP_VERSION = 'v1.6.5';