Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5610cf156 | ||
|
|
ae932a9aa9 | ||
|
|
a106d47f77 | ||
|
|
41d464a4b3 | ||
|
|
9e69f19e23 | ||
|
|
1df7bc3f87 | ||
|
|
e5f9831d73 | ||
|
|
553bc84404 | ||
|
|
88a8857a6f | ||
|
|
edefaaca36 | ||
|
|
ef0a8da696 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
github: [error311]
|
||||
ko_fi: error311
|
||||
56
.github/workflows/sync-changelog.yml
vendored
56
.github/workflows/sync-changelog.yml
vendored
@@ -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
|
||||
|
||||
84
CHANGELOG.md
84
CHANGELOG.md
@@ -1,5 +1,89 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 10/24/2025 (v1.6.6)
|
||||
|
||||
release(v1.6.6): header-mounted toggle, dark-mode polish, persistent layout, and ACL fix
|
||||
|
||||
- dragAndDrop: mount zones toggle beside header logo (absolute, non-scrolling);
|
||||
stop click propagation so it doesn’t trigger the logo link; theme-aware styling
|
||||
- live updates via MutationObserver; snapshot card locations on drop and restore
|
||||
on load (prevents sidebar reset); guard first-run defaults with
|
||||
`layoutDefaultApplied_v1`; small/medium layout tweaks & refactors.
|
||||
- CSS: switch toggle icon to CSS variable (`--toggle-icon-color`) with dark-mode
|
||||
override; remove hardcoded `!important`.
|
||||
- API (capabilities.php): remove unused `disableUpload` flag from `canUpload`
|
||||
and flags payload to resolve undefined variable warning.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
50
README.md
50
README.md
@@ -7,6 +7,8 @@
|
||||
[](https://demo.filerise.net)
|
||||
[](https://github.com/error311/FileRise/releases)
|
||||
[](LICENSE)
|
||||
[](https://github.com/sponsors/error311)
|
||||
[](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)
|
||||
|
||||
@@ -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 server’s 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, you’ll be prompted to create an **Admin account**. Then use **User Management** to add more users.
|
||||
|
||||
@@ -247,6 +269,13 @@ Browse to your FileRise URL; you’ll 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; you’ll 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).
|
||||
|
||||
@@ -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>
|
||||
|
||||
# -----------------------------
|
||||
|
||||
@@ -153,7 +153,6 @@ if ($folder !== 'root') {
|
||||
$perms = loadPermsFor($username);
|
||||
$isAdmin = ACL::isAdmin($perms);
|
||||
$readOnly = !empty($perms['readOnly']);
|
||||
$disableUp = !empty($perms['disableUpload']);
|
||||
$inScope = inUserFolderScope($folder, $username, $perms, $isAdmin);
|
||||
|
||||
// --- ACL base abilities ---
|
||||
@@ -178,7 +177,7 @@ $gShareFolder = $isAdmin || ACL::canShareFolder($username, $perms, $folder);
|
||||
|
||||
// --- Apply scope + flags to effective UI actions ---
|
||||
$canView = $canViewBase && $inScope; // keep scope for folder-only
|
||||
$canUpload = $gUploadBase && !$readOnly && !$disableUpload && $inScope;
|
||||
$canUpload = $gUploadBase && !$readOnly && $inScope;
|
||||
$canCreate = $canManageBase && !$readOnly && $inScope; // Create **folder**
|
||||
$canRename = $canManageBase && !$readOnly && $inScope; // Rename **folder**
|
||||
$canDelete = $gDeleteBase && !$readOnly && $inScope;
|
||||
@@ -213,7 +212,6 @@ echo json_encode([
|
||||
'flags' => [
|
||||
//'folderOnly' => !empty($perms['folderOnly']) || !empty($perms['userFolderOnly']) || !empty($perms['UserFolderOnly']),
|
||||
'readOnly' => $readOnly,
|
||||
'disableUpload' => $disableUp,
|
||||
],
|
||||
'owner' => $owner,
|
||||
|
||||
|
||||
@@ -2318,11 +2318,14 @@ body.dark-mode { --perm-caret: #ccc; } /* dark */
|
||||
background-color 160ms cubic-bezier(.2,.0,.2,1);
|
||||
}
|
||||
|
||||
:root { --toggle-icon-color: #333; }
|
||||
body.dark-mode { --toggle-icon-color: #eee; }
|
||||
|
||||
#zonesToggleFloating .material-icons,
|
||||
#zonesToggleFloating .material-icons-outlined,
|
||||
#sidebarToggleFloating .material-icons,
|
||||
#sidebarToggleFloating .material-icons-outlined {
|
||||
color: #333 !important;
|
||||
color: var(--toggle-icon-color);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
|
||||
@@ -563,6 +563,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js"></script>
|
||||
<script type="module" src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -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.2";
|
||||
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' });
|
||||
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 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();
|
||||
}
|
||||
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 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)}`, {
|
||||
@@ -673,8 +709,10 @@ function renderFolderGrantsUI(username, container, folders, grants) {
|
||||
container.appendChild(list);
|
||||
|
||||
const headerHtml = `
|
||||
<div class="folder-access-header">
|
||||
<div title="${tf('folder_help', 'Folder path within FileRise')}">${tf('folder', 'Folder')}</div>
|
||||
<div class="folder-access-header">
|
||||
<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 (everyone’s 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 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 });
|
||||
});
|
||||
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"
|
||||
@@ -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,61 +1250,73 @@ 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);
|
||||
row.style.padding = "6px 0";
|
||||
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 = `
|
||||
<div class="user-perm-header" tabindex="0" role="button" aria-expanded="false"
|
||||
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>
|
||||
</div>
|
||||
<div class="user-perm-details" style="display:none; margin:8px 0 12px;">
|
||||
<div class="folder-grants-box" data-loaded="0"></div>
|
||||
</div>
|
||||
`;
|
||||
row.innerHTML = `
|
||||
<div class="user-perm-header" tabindex="0" role="button" aria-expanded="false"
|
||||
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>
|
||||
${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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const header = row.querySelector(".user-perm-header");
|
||||
const details = row.querySelector(".user-perm-details");
|
||||
const caret = row.querySelector(".perm-caret");
|
||||
const grantsBox = row.querySelector(".folder-grants-box");
|
||||
const header = row.querySelector(".user-perm-header");
|
||||
const details = row.querySelector(".user-perm-details");
|
||||
const caret = row.querySelector(".perm-caret");
|
||||
const grantsBox = row.querySelector(".folder-grants-box");
|
||||
|
||||
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);
|
||||
grantsBox.dataset.loaded = "1";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
grantsBox.innerHTML = `<div class="muted">${tf("error_loading_user_grants", "Error loading user grants")}</div>`;
|
||||
}
|
||||
async function ensureLoaded() {
|
||||
if (grantsBox.dataset.loaded === "1") return;
|
||||
try {
|
||||
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);
|
||||
grantsBox.innerHTML = `<div class="muted">${tf("error_loading_user_grants", "Error loading user grants")}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleOpen() {
|
||||
const willShow = details.style.display === "none";
|
||||
details.style.display = willShow ? "block" : "none";
|
||||
header.setAttribute("aria-expanded", willShow ? "true" : "false");
|
||||
caret.style.transform = willShow ? "rotate(0deg)" : "rotate(-90deg)";
|
||||
if (willShow) ensureLoaded();
|
||||
}
|
||||
function toggleOpen() {
|
||||
const willShow = details.style.display === "none";
|
||||
details.style.display = willShow ? "block" : "none";
|
||||
header.setAttribute("aria-expanded", willShow ? "true" : "false");
|
||||
caret.style.transform = willShow ? "rotate(0deg)" : "rotate(-90deg)";
|
||||
if (willShow) ensureLoaded();
|
||||
}
|
||||
|
||||
header.addEventListener("click", toggleOpen);
|
||||
header.addEventListener("keydown", e => {
|
||||
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleOpen(); }
|
||||
});
|
||||
header.addEventListener("click", toggleOpen);
|
||||
header.addEventListener("keydown", e => {
|
||||
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleOpen(); }
|
||||
});
|
||||
|
||||
listContainer.appendChild(row);
|
||||
});
|
||||
listContainer.appendChild(row);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
|
||||
|
||||
@@ -8,11 +8,263 @@
|
||||
const MEDIUM_MIN = 1205; // matches your small-screen cutoff
|
||||
const MEDIUM_MAX = 1600; // tweak as you like
|
||||
|
||||
const TOGGLE_TOP_PX = 10;
|
||||
const TOGGLE_TOP_PX = 10;
|
||||
const TOGGLE_LEFT_PX = 100;
|
||||
|
||||
const TOGGLE_ICON_OPEN = 'view_sidebar';
|
||||
const TOGGLE_ICON_CLOSED = 'menu';
|
||||
const TOGGLE_ICON_OPEN = 'view_sidebar';
|
||||
const TOGGLE_ICON_CLOSED = 'menu';
|
||||
|
||||
// Cards we manage
|
||||
const KNOWN_CARD_IDS = ['uploadCard', 'folderManagementCard'];
|
||||
|
||||
const CARD_IDS = ['uploadCard', 'folderManagementCard'];
|
||||
|
||||
function isDarkMode() {
|
||||
return document.body.classList.contains('dark-mode');
|
||||
}
|
||||
|
||||
function themeToggleButton(btn) {
|
||||
if (!btn) return;
|
||||
if (isDarkMode()) {
|
||||
btn.style.background = '#2c2c2c';
|
||||
btn.style.border = '1px solid #555';
|
||||
btn.style.boxShadow = '0 2px 6px rgba(0,0,0,.35)';
|
||||
btn.style.color = '#e0e0e0'; // <- material icon inherits this
|
||||
} else {
|
||||
btn.style.background = '#fff';
|
||||
btn.style.border = '1px solid #ccc';
|
||||
btn.style.boxShadow = '0 2px 6px rgba(0,0,0,.15)';
|
||||
btn.style.color = '#222'; // <- material icon inherits this
|
||||
}
|
||||
}
|
||||
|
||||
function getKnownCards() {
|
||||
return CARD_IDS
|
||||
.map(id => document.getElementById(id))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// Save current container for each card so we can restore after refresh.
|
||||
function snapshotZoneLocations() {
|
||||
const snap = {};
|
||||
getKnownCards().forEach(card => {
|
||||
const p = card.parentNode;
|
||||
snap[card.id] = p && p.id ? p.id : '';
|
||||
});
|
||||
localStorage.setItem('zonesSnapshot', JSON.stringify(snap));
|
||||
}
|
||||
|
||||
// Move a card to default expanded spot (your request: sidebar is default).
|
||||
function moveCardToSidebarDefault(card) {
|
||||
const sidebar = getSidebar();
|
||||
if (sidebar) {
|
||||
sidebar.appendChild(card);
|
||||
card.style.width = '100%';
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any header icon/modal for a card (so it truly leaves header mode).
|
||||
function stripHeaderArtifacts(card) {
|
||||
if (card.headerIconButton) {
|
||||
if (card.headerIconButton.modalInstance) {
|
||||
try { card.headerIconButton.modalInstance.remove(); } catch { }
|
||||
}
|
||||
try { card.headerIconButton.remove(); } catch { }
|
||||
card.headerIconButton = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore cards after “expand” (toggle off) or after refresh.
|
||||
// - If we have a snapshot, use it.
|
||||
// - If not, put all cards in the sidebar (your default).
|
||||
function restoreCardsFromSnapshot() {
|
||||
const sidebar = getSidebar();
|
||||
const leftCol = document.getElementById('leftCol');
|
||||
const rightCol = document.getElementById('rightCol');
|
||||
|
||||
let snap = {};
|
||||
try { snap = JSON.parse(localStorage.getItem('zonesSnapshot') || '{}'); } catch { }
|
||||
|
||||
getKnownCards().forEach(card => {
|
||||
stripHeaderArtifacts(card);
|
||||
const destId = snap[card.id] || 'sidebarDropArea'; // fallback to sidebar
|
||||
const dest =
|
||||
destId === 'leftCol' ? leftCol :
|
||||
destId === 'rightCol' ? rightCol :
|
||||
destId === 'sidebarDropArea' ? sidebar :
|
||||
sidebar; // final fallback
|
||||
card.style.width = '';
|
||||
card.style.minWidth = '';
|
||||
if (dest) dest.appendChild(card);
|
||||
});
|
||||
|
||||
// Clear header icons storage because we’re expanded.
|
||||
localStorage.removeItem('headerOrder');
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (headerDropArea) headerDropArea.innerHTML = '';
|
||||
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
ensureZonesToggle();
|
||||
updateZonesToggleUI();
|
||||
}
|
||||
|
||||
// Read the saved snapshot (or {} if none)
|
||||
function readZonesSnapshot() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('zonesSnapshot') || '{}');
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Move a card into the header zone as an icon (uses your existing helper)
|
||||
function moveCardToHeader(card) {
|
||||
// If it's already in header icon form, skip
|
||||
if (card.headerIconButton && card.headerIconButton.parentNode) return;
|
||||
insertCardInHeader(card, null);
|
||||
}
|
||||
|
||||
// Collapse behavior: snapshot locations, then move all known cards to header as icons
|
||||
function collapseCardsToHeader() {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (headerDropArea) headerDropArea.style.display = 'inline-flex'; // NEW
|
||||
|
||||
getKnownCards().forEach(card => {
|
||||
if (!card.headerIconButton) insertCardInHeader(card, null);
|
||||
});
|
||||
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
ensureZonesToggle();
|
||||
updateZonesToggleUI();
|
||||
}
|
||||
|
||||
// Clean up any header icon (button + modal) attached to a card
|
||||
function removeHeaderIconForCard(card) {
|
||||
if (card.headerIconButton) {
|
||||
const btn = card.headerIconButton;
|
||||
const modal = btn.modalInstance;
|
||||
if (btn.parentNode) btn.parentNode.removeChild(btn);
|
||||
if (modal && modal.parentNode) modal.parentNode.removeChild(modal);
|
||||
card.headerIconButton = null;
|
||||
}
|
||||
}
|
||||
|
||||
function applySnapshotIfPresent() {
|
||||
const snap = readZonesSnapshot();
|
||||
const keys = Object.keys(snap || {});
|
||||
if (!keys.length) return false;
|
||||
|
||||
const sidebar = getSidebar();
|
||||
const leftCol = document.getElementById('leftCol');
|
||||
const rightCol = document.getElementById('rightCol');
|
||||
|
||||
getKnownCards().forEach(card => {
|
||||
const destId = snap[card.id];
|
||||
const dest =
|
||||
destId === 'leftCol' ? leftCol :
|
||||
destId === 'rightCol' ? rightCol :
|
||||
destId === 'sidebarDropArea' ? sidebar : null;
|
||||
if (dest) {
|
||||
// clear sticky widths if coming from sidebar/header
|
||||
card.style.width = '';
|
||||
card.style.minWidth = '';
|
||||
dest.appendChild(card);
|
||||
}
|
||||
});
|
||||
|
||||
// prevent first-run default from stomping this on reload
|
||||
localStorage.setItem('layoutDefaultApplied_v1', '1');
|
||||
return true;
|
||||
}
|
||||
|
||||
// New: small-screen detector
|
||||
function isSmallScreen() { return window.innerWidth < MEDIUM_MIN; }
|
||||
|
||||
// New: remember which cards were in the sidebar right before we go small
|
||||
const RESPONSIVE_SNAPSHOT_KEY = 'responsiveSidebarSnapshot';
|
||||
|
||||
function snapshotSidebarCardsForResponsive() {
|
||||
const sb = getSidebar();
|
||||
if (!sb) return;
|
||||
const ids = Array.from(sb.querySelectorAll('#uploadCard, #folderManagementCard'))
|
||||
.map(el => el.id);
|
||||
localStorage.setItem(RESPONSIVE_SNAPSHOT_KEY, JSON.stringify(ids));
|
||||
}
|
||||
|
||||
function readResponsiveSnapshot() {
|
||||
try { return JSON.parse(localStorage.getItem(RESPONSIVE_SNAPSHOT_KEY) || '[]'); }
|
||||
catch { return []; }
|
||||
}
|
||||
|
||||
function clearResponsiveSnapshot() {
|
||||
localStorage.removeItem(RESPONSIVE_SNAPSHOT_KEY);
|
||||
}
|
||||
|
||||
// New: deterministic mapping from card -> top column
|
||||
function moveCardToTopByMapping(card) {
|
||||
const leftCol = document.getElementById('leftCol');
|
||||
const rightCol = document.getElementById('rightCol');
|
||||
if (!leftCol || !rightCol) return;
|
||||
|
||||
const target = (card.id === 'uploadCard') ? leftCol :
|
||||
(card.id === 'folderManagementCard') ? rightCol : leftCol;
|
||||
|
||||
// clear any sticky widths from sidebar/header
|
||||
card.style.width = '';
|
||||
card.style.minWidth = '';
|
||||
target.appendChild(card);
|
||||
card.dataset.originalContainerId = target.id;
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
|
||||
// New: move all sidebar cards to top (used when we cross into small)
|
||||
function moveAllSidebarCardsToTop() {
|
||||
const sb = getSidebar();
|
||||
if (!sb) return;
|
||||
const cards = Array.from(sb.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||
cards.forEach(moveCardToTopByMapping);
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
}
|
||||
|
||||
// New: enforce responsive behavior (sidebar disabled on small screens)
|
||||
let __lastIsSmall = null;
|
||||
|
||||
function enforceResponsiveZones() {
|
||||
const isSmall = isSmallScreen();
|
||||
const sidebar = getSidebar();
|
||||
const topZone = getTopZone();
|
||||
|
||||
if (isSmall && __lastIsSmall !== true) {
|
||||
// entering small: remember what was in sidebar, move them up, hide sidebar
|
||||
snapshotSidebarCardsForResponsive();
|
||||
moveAllSidebarCardsToTop();
|
||||
if (sidebar) sidebar.style.display = 'none';
|
||||
if (topZone) topZone.style.display = ''; // ensure visible
|
||||
__lastIsSmall = true;
|
||||
} else if (!isSmall && __lastIsSmall !== false) {
|
||||
// leaving small: restore only what used to be in the sidebar
|
||||
const ids = readResponsiveSnapshot();
|
||||
const sb = getSidebar();
|
||||
ids.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card && sb && !sb.contains(card)) {
|
||||
sb.appendChild(card);
|
||||
card.style.width = '100%';
|
||||
}
|
||||
});
|
||||
clearResponsiveSnapshot();
|
||||
// show sidebar again if panels aren’t collapsed
|
||||
if (sidebar) sidebar.style.display = isZonesCollapsed() ? 'none' : 'block';
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
__lastIsSmall = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateSidebarToggleUI() {
|
||||
const btn = document.getElementById('sidebarToggleFloating');
|
||||
@@ -22,9 +274,8 @@ function updateSidebarToggleUI() {
|
||||
if (!hasSidebarCards()) { btn.remove(); return; }
|
||||
|
||||
const collapsed = isSidebarCollapsed();
|
||||
btn.innerHTML = `<i class="material-icons" aria-hidden="true">${
|
||||
collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN
|
||||
}</i>`;
|
||||
btn.innerHTML = `<i class="material-icons" aria-hidden="true">${collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN
|
||||
}</i>`;
|
||||
btn.title = collapsed ? 'Show sidebar' : 'Hide sidebar';
|
||||
btn.style.display = 'block';
|
||||
btn.classList.toggle('toggle-ping', collapsed);
|
||||
@@ -43,28 +294,45 @@ function hasTopZoneCards() {
|
||||
|
||||
// Both cards are in the top zone (upload + folder)
|
||||
function allCardsInTopZone() {
|
||||
const tz = getTopZone();
|
||||
if (!tz) return false;
|
||||
const hasUpload = !!tz.querySelector('#uploadCard');
|
||||
const hasFolder = !!tz.querySelector('#folderManagementCard');
|
||||
return hasUpload && hasFolder;
|
||||
}
|
||||
const tz = getTopZone();
|
||||
if (!tz) return false;
|
||||
const hasUpload = !!tz.querySelector('#uploadCard');
|
||||
const hasFolder = !!tz.querySelector('#folderManagementCard');
|
||||
return hasUpload && hasFolder;
|
||||
}
|
||||
|
||||
function isZonesCollapsed() {
|
||||
return localStorage.getItem('zonesCollapsed') === '1';
|
||||
}
|
||||
function setZonesCollapsed(collapsed) {
|
||||
localStorage.setItem('zonesCollapsed', collapsed ? '1' : '0');
|
||||
applyZonesCollapsed();
|
||||
|
||||
if (collapsed) {
|
||||
// Remember where cards were, then show them as header icons
|
||||
snapshotZoneLocations();
|
||||
collapseCardsToHeader(); // your existing helper that calls insertCardInHeader(...)
|
||||
} else {
|
||||
// Expand: bring cards back
|
||||
restoreCardsFromSnapshot();
|
||||
|
||||
// Ensure zones are visible right away after expand
|
||||
const sidebar = getSidebar();
|
||||
const topZone = getTopZone();
|
||||
if (sidebar) sidebar.style.display = 'block';
|
||||
if (topZone) topZone.style.display = '';
|
||||
}
|
||||
|
||||
ensureZonesToggle();
|
||||
updateZonesToggleUI();
|
||||
}
|
||||
|
||||
function applyZonesCollapsed() {
|
||||
const collapsed = isZonesCollapsed();
|
||||
const sidebar = getSidebar();
|
||||
const topZone = getTopZone();
|
||||
|
||||
if (sidebar) sidebar.style.display = collapsed ? 'none' : (hasSidebarCards() ? 'block' : 'none');
|
||||
if (topZone) topZone.style.display = collapsed ? 'none' : (hasTopZoneCards() ? '' : '');
|
||||
if (topZone) topZone.style.display = collapsed ? 'none' : (hasTopZoneCards() ? '' : '');
|
||||
}
|
||||
|
||||
function isMediumScreen() {
|
||||
@@ -100,24 +368,70 @@ function applySidebarCollapsed() {
|
||||
sidebar.style.display = collapsed ? 'none' : 'block';
|
||||
}
|
||||
|
||||
function ensureZonesToggle() {
|
||||
// show only if at least one zone *can* show a card
|
||||
const shouldShow = hasSidebarCards() || hasTopZoneCards();
|
||||
function getHeaderHost() {
|
||||
// 1) exact structure you shared
|
||||
let host = document.querySelector('.header-container .header-left');
|
||||
// 2) fallback to header root
|
||||
if (!host) host = document.querySelector('.header-container');
|
||||
// 3) last resort
|
||||
if (!host) host = document.querySelector('header');
|
||||
return host || document.body;
|
||||
}
|
||||
|
||||
let btn = document.getElementById('sidebarToggleFloating');
|
||||
if (!shouldShow) {
|
||||
if (btn) btn.remove();
|
||||
return;
|
||||
function mountHeaderToggle(btn) {
|
||||
const host = document.querySelector('.header-left');
|
||||
const logoA = host?.querySelector('a');
|
||||
if (!host) return;
|
||||
|
||||
// ensure positioning context
|
||||
if (getComputedStyle(host).position === 'static') host.style.position = 'relative';
|
||||
|
||||
if (logoA) {
|
||||
logoA.insertAdjacentElement('afterend', btn); // sibling of <a>, not inside it
|
||||
} else {
|
||||
host.appendChild(btn);
|
||||
}
|
||||
|
||||
Object.assign(btn.style, {
|
||||
position: 'absolute',
|
||||
left: '100px', // adjust position beside the logo
|
||||
top: '10px',
|
||||
zIndex: '10010',
|
||||
pointerEvents: 'auto'
|
||||
});
|
||||
}
|
||||
|
||||
function ensureZonesToggle() {
|
||||
let btn = document.getElementById('sidebarToggleFloating');
|
||||
const host = getHeaderHost();
|
||||
if (!host) return;
|
||||
|
||||
// ensure the host is a positioning context
|
||||
const hostStyle = getComputedStyle(host);
|
||||
if (hostStyle.position === 'static') {
|
||||
host.style.position = 'relative';
|
||||
}
|
||||
|
||||
if (!btn) {
|
||||
btn = document.createElement('button');
|
||||
|
||||
btn.id = 'sidebarToggleFloating';
|
||||
btn.type = 'button';
|
||||
btn.type = 'button'; // not a submit
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // don't bubble into the <a href="index.html">
|
||||
setSidebarCollapsed(!isSidebarCollapsed());
|
||||
updateSidebarToggleUI(); // refresh icon/title
|
||||
});
|
||||
['mousedown','mouseup','pointerdown','pointerup'].forEach(evt =>
|
||||
btn.addEventListener(evt, (e) => e.stopPropagation())
|
||||
);
|
||||
btn.setAttribute('aria-label', 'Toggle panels');
|
||||
|
||||
Object.assign(btn.style, {
|
||||
position: 'fixed',
|
||||
left: `${TOGGLE_LEFT_PX}px`,
|
||||
top: `${TOGGLE_TOP_PX}px`,
|
||||
position: 'absolute', // <-- key change (was fixed)
|
||||
top: '8px', // adjust to line up with header content
|
||||
left: '100px', // place to the right of your logo; tweak as needed
|
||||
zIndex: '1000',
|
||||
width: '38px',
|
||||
height: '38px',
|
||||
@@ -126,36 +440,49 @@ function ensureZonesToggle() {
|
||||
background: '#fff',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,.15)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0',
|
||||
lineHeight: '0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0',
|
||||
lineHeight: '0'
|
||||
});
|
||||
|
||||
// dark-mode polish (optional)
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
btn.style.background = '#2c2c2c';
|
||||
btn.style.border = '1px solid #555';
|
||||
btn.style.boxShadow = '0 2px 6px rgba(0,0,0,.35)';
|
||||
btn.style.color = '#e0e0e0';
|
||||
}
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
setZonesCollapsed(!isZonesCollapsed());
|
||||
});
|
||||
document.body.appendChild(btn);
|
||||
|
||||
// Insert right after the logo if present, else just append to host
|
||||
const afterLogo = host.querySelector('.header-logo');
|
||||
if (afterLogo && afterLogo.parentNode) {
|
||||
afterLogo.parentNode.insertBefore(btn, afterLogo.nextSibling);
|
||||
} else {
|
||||
host.appendChild(btn);
|
||||
}
|
||||
themeToggleButton(btn);
|
||||
}
|
||||
|
||||
updateZonesToggleUI();
|
||||
}
|
||||
|
||||
function updateZonesToggleUI() {
|
||||
const btn = document.getElementById('sidebarToggleFloating');
|
||||
if (!btn) return;
|
||||
|
||||
// if neither zone has cards, remove the toggle
|
||||
if (!hasSidebarCards() && !hasTopZoneCards()) {
|
||||
btn.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Never remove the button just because cards are in header.
|
||||
const collapsed = isZonesCollapsed();
|
||||
const iconName = collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN;
|
||||
btn.innerHTML = `<i class="material-icons toggle-icon" aria-hidden="true">${iconName}</i>`;
|
||||
btn.title = collapsed ? 'Show panels' : 'Hide panels';
|
||||
btn.style.display = 'block';
|
||||
|
||||
// Rotate the icon 90° when BOTH cards are in the top zone and panels are open
|
||||
const iconEl = btn.querySelector('.toggle-icon');
|
||||
if (iconEl) {
|
||||
iconEl.style.transition = 'transform 0.2s ease';
|
||||
@@ -167,8 +494,21 @@ function updateZonesToggleUI() {
|
||||
iconEl.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
themeToggleButton(btn);
|
||||
}
|
||||
|
||||
(function watchThemeChanges() {
|
||||
const obs = new MutationObserver((muts) => {
|
||||
for (const m of muts) {
|
||||
if (m.type === 'attributes' && m.attributeName === 'class') {
|
||||
const btn = document.getElementById('sidebarToggleFloating');
|
||||
if (btn) themeToggleButton(btn);
|
||||
}
|
||||
}
|
||||
});
|
||||
obs.observe(document.body, { attributes: true });
|
||||
})();
|
||||
|
||||
// create a small floating toggle button (no HTML edits needed)
|
||||
function ensureSidebarToggle() {
|
||||
const sidebar = getSidebar();
|
||||
@@ -224,47 +564,61 @@ export function loadSidebarOrder() {
|
||||
const sidebar = getSidebar();
|
||||
if (!sidebar) return;
|
||||
|
||||
const defaultAppliedKey = 'layoutDefaultApplied_v1';
|
||||
const defaultAlready = localStorage.getItem(defaultAppliedKey) === '1';
|
||||
|
||||
const orderStr = localStorage.getItem('sidebarOrder');
|
||||
const headerOrderStr = localStorage.getItem('headerOrder');
|
||||
const defaultAppliedKey = 'layoutDefaultApplied_v1'; // bump if logic changes
|
||||
|
||||
// If we have a saved order (sidebar), honor it as before
|
||||
if (orderStr) {
|
||||
const order = JSON.parse(orderStr || '[]');
|
||||
if (Array.isArray(order) && order.length > 0) {
|
||||
const mainWrapper = document.querySelector('.main-wrapper');
|
||||
if (mainWrapper) mainWrapper.style.display = 'flex';
|
||||
order.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card && card.parentNode?.id !== 'sidebarDropArea') {
|
||||
sidebar.appendChild(card);
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
});
|
||||
updateSidebarVisibility();
|
||||
//applySidebarCollapsed(); // NEW: honor collapsed state
|
||||
//ensureSidebarToggle(); // NEW: inject toggle
|
||||
applyZonesCollapsed();
|
||||
ensureZonesToggle();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No sidebar order saved yet: if user has header icons saved, do nothing (they've customized)
|
||||
const headerOrder = JSON.parse(headerOrderStr || '[]');
|
||||
if (Array.isArray(headerOrder) && headerOrder.length > 0) {
|
||||
if (applySnapshotIfPresent()) {
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
//applySidebarCollapsed();
|
||||
//ensureSidebarToggle();
|
||||
applyZonesCollapsed();
|
||||
ensureZonesToggle();
|
||||
ensureZonesToggle();
|
||||
return;
|
||||
}
|
||||
|
||||
// One-time default: on medium screens, start cards in the sidebar
|
||||
const alreadyApplied = localStorage.getItem(defaultAppliedKey) === '1';
|
||||
if (!alreadyApplied && isMediumScreen()) {
|
||||
// Only apply the one-time default if *not* initialized yet
|
||||
if (!defaultAlready &&
|
||||
((!orderStr || !JSON.parse(orderStr || '[]').length) &&
|
||||
(!headerOrderStr || !JSON.parse(headerOrderStr || '[]').length))) {
|
||||
|
||||
const isLargeEnough = window.innerWidth >= MEDIUM_MIN;
|
||||
if (isLargeEnough) {
|
||||
const mainWrapper = document.querySelector('.main-wrapper');
|
||||
if (mainWrapper) mainWrapper.style.display = 'flex';
|
||||
|
||||
const moved = [];
|
||||
['uploadCard', 'folderManagementCard'].forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card && card.parentNode?.id !== 'sidebarDropArea') {
|
||||
card.style.width = '';
|
||||
card.style.minWidth = '';
|
||||
getSidebar().appendChild(card);
|
||||
animateVerticalSlide(card);
|
||||
moved.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
if (moved.length) {
|
||||
localStorage.setItem('sidebarOrder', JSON.stringify(moved));
|
||||
}
|
||||
}
|
||||
|
||||
// Mark initialized so this default never fires again
|
||||
localStorage.setItem(defaultAppliedKey, '1');
|
||||
}
|
||||
|
||||
// If user has header icons saved, honor that and bail
|
||||
const headerOrder = JSON.parse(headerOrderStr || '[]');
|
||||
if (Array.isArray(headerOrder) && headerOrder.length > 0) {
|
||||
updateSidebarVisibility();
|
||||
applyZonesCollapsed();
|
||||
ensureZonesToggle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!defaultAlready && isMediumScreen()) {
|
||||
const mainWrapper = document.querySelector('.main-wrapper');
|
||||
if (mainWrapper) mainWrapper.style.display = 'flex';
|
||||
|
||||
@@ -281,42 +635,34 @@ export function loadSidebarOrder() {
|
||||
|
||||
if (moved.length) {
|
||||
localStorage.setItem('sidebarOrder', JSON.stringify(moved));
|
||||
localStorage.setItem(defaultAppliedKey, '1');
|
||||
localStorage.setItem(defaultAppliedKey, '1'); // mark initialized
|
||||
}
|
||||
}
|
||||
|
||||
updateSidebarVisibility();
|
||||
//applySidebarCollapsed();
|
||||
//ensureSidebarToggle();
|
||||
applyZonesCollapsed();
|
||||
ensureZonesToggle();
|
||||
ensureZonesToggle();
|
||||
}
|
||||
|
||||
export function loadHeaderOrder() {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (!headerDropArea) return;
|
||||
|
||||
// 1) Clear out any icons that might already be in the drop area
|
||||
headerDropArea.innerHTML = '';
|
||||
|
||||
// 2) Read the saved array (or empty array if invalid/missing)
|
||||
let stored;
|
||||
try {
|
||||
stored = JSON.parse(localStorage.getItem('headerOrder') || '[]');
|
||||
} catch {
|
||||
stored = [];
|
||||
// If panels are expanded, do not re-create header icons.
|
||||
if (!isZonesCollapsed()) {
|
||||
headerDropArea.innerHTML = '';
|
||||
localStorage.removeItem('headerOrder');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Deduplicate IDs
|
||||
headerDropArea.innerHTML = '';
|
||||
let stored;
|
||||
try { stored = JSON.parse(localStorage.getItem('headerOrder') || '[]'); } catch { stored = []; }
|
||||
const uniqueIds = Array.from(new Set(stored));
|
||||
|
||||
// 4) Re-insert exactly one icon per saved card ID
|
||||
uniqueIds.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card) insertCardInHeader(card, null);
|
||||
});
|
||||
|
||||
// 5) Persist the cleaned, deduped list back to storage
|
||||
localStorage.setItem('headerOrder', JSON.stringify(uniqueIds));
|
||||
}
|
||||
|
||||
@@ -332,17 +678,20 @@ function updateSidebarVisibility() {
|
||||
sidebar.style.height = '';
|
||||
|
||||
if (anyCards) {
|
||||
sidebar.classList.add('active');
|
||||
// respect the unified zones-collapsed switch
|
||||
sidebar.style.display = isZonesCollapsed() ? 'none' : 'block';
|
||||
} else {
|
||||
sidebar.classList.remove('active');
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
sidebar.classList.add('active');
|
||||
// respect the unified zones-collapsed switch
|
||||
sidebar.style.display = isZonesCollapsed() ? 'none' : 'block';
|
||||
} else {
|
||||
sidebar.classList.remove('active');
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
|
||||
// Save order and update toggle visibility
|
||||
saveSidebarOrder();
|
||||
ensureZonesToggle(); // will hide/remove the button if no cards
|
||||
// Mark layout initialized so the first-run default won't fire on reload
|
||||
localStorage.setItem('layoutDefaultApplied_v1', '1');
|
||||
|
||||
ensureZonesToggle();
|
||||
}
|
||||
|
||||
// NEW: Save header order to localStorage.
|
||||
@@ -358,19 +707,22 @@ function saveHeaderOrder() {
|
||||
|
||||
// Internal helper: update top zone layout (center a card if one column is empty).
|
||||
function updateTopZoneLayout() {
|
||||
const topZone = getTopZone();
|
||||
const leftCol = document.getElementById('leftCol');
|
||||
const rightCol = document.getElementById('rightCol');
|
||||
|
||||
const leftIsEmpty = !leftCol?.querySelector('#uploadCard');
|
||||
const rightIsEmpty = !rightCol?.querySelector('#folderManagementCard');
|
||||
const hasUpload = !!topZone?.querySelector('#uploadCard');
|
||||
const hasFolder = !!topZone?.querySelector('#folderManagementCard');
|
||||
|
||||
if (leftCol && rightCol) {
|
||||
if (leftIsEmpty && !rightIsEmpty) {
|
||||
leftCol.style.display = 'none';
|
||||
rightCol.style.margin = '0 auto';
|
||||
} else if (rightIsEmpty && !leftIsEmpty) {
|
||||
if (hasUpload && !hasFolder) {
|
||||
rightCol.style.display = 'none';
|
||||
leftCol.style.margin = '0 auto';
|
||||
leftCol.style.display = '';
|
||||
} else if (!hasUpload && hasFolder) {
|
||||
leftCol.style.display = 'none';
|
||||
rightCol.style.margin = '0 auto';
|
||||
rightCol.style.display = '';
|
||||
} else {
|
||||
leftCol.style.display = '';
|
||||
rightCol.style.display = '';
|
||||
@@ -378,6 +730,9 @@ function updateTopZoneLayout() {
|
||||
rightCol.style.margin = '';
|
||||
}
|
||||
}
|
||||
|
||||
// hide whole top row when empty (kills the gap)
|
||||
if (topZone) topZone.style.display = (hasUpload || hasFolder) ? '' : 'none';
|
||||
}
|
||||
|
||||
// When a card is being dragged, if the top drop zone is empty, set its min-height.
|
||||
@@ -422,6 +777,7 @@ function animateVerticalSlide(card) {
|
||||
function insertCardInSidebar(card, event) {
|
||||
const sidebar = getSidebar();
|
||||
if (!sidebar) return;
|
||||
|
||||
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||
let inserted = false;
|
||||
for (const currentCard of existingCards) {
|
||||
@@ -433,14 +789,19 @@ function insertCardInSidebar(card, event) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
sidebar.appendChild(card);
|
||||
}
|
||||
// Ensure card fills the sidebar.
|
||||
if (!inserted) sidebar.appendChild(card);
|
||||
|
||||
// Make it fill the sidebar and clear any sticky width from header/top zone.
|
||||
card.style.width = '100%';
|
||||
removeHeaderIconForCard(card); // NEW: remove any header artifacts
|
||||
card.dataset.originalContainerId = 'sidebarDropArea';
|
||||
animateVerticalSlide(card);
|
||||
// if user dropped into sidebar, auto-un-collapse if currently collapsed
|
||||
if (isZonesCollapsed()) setZonesCollapsed(false);
|
||||
|
||||
// SAVE order & refresh minimal UI, but DO NOT collapse/restore here:
|
||||
saveSidebarOrder();
|
||||
updateSidebarVisibility();
|
||||
ensureZonesToggle();
|
||||
updateZonesToggleUI();
|
||||
}
|
||||
|
||||
// Internal helper: save the current sidebar card order to localStorage.
|
||||
@@ -472,16 +833,55 @@ function moveSidebarCardsToTop() {
|
||||
}
|
||||
|
||||
// Listen for window resize to automatically move sidebar cards back to top on small screens.
|
||||
window.addEventListener('resize', function () {
|
||||
if (window.innerWidth < 1205) {
|
||||
moveSidebarCardsToTop();
|
||||
(function () {
|
||||
let rAF = null;
|
||||
window.addEventListener('resize', () => {
|
||||
if (rAF) cancelAnimationFrame(rAF);
|
||||
rAF = requestAnimationFrame(() => {
|
||||
enforceResponsiveZones();
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
function showTopZoneWhileDragging() {
|
||||
const topZone = getTopZone();
|
||||
if (!topZone) return;
|
||||
topZone.style.display = ''; // make it droppable
|
||||
// add a temporary placeholder only if empty
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
let ph = topZone.querySelector('.placeholder');
|
||||
if (!ph) {
|
||||
ph = document.createElement('div');
|
||||
ph.className = 'placeholder';
|
||||
ph.style.visibility = 'hidden';
|
||||
ph.style.display = 'block';
|
||||
ph.style.width = '100%';
|
||||
ph.style.height = '375px';
|
||||
topZone.appendChild(ph);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupTopZoneAfterDrop() {
|
||||
const topZone = getTopZone();
|
||||
if (!topZone) return;
|
||||
|
||||
// remove placeholder and highlight/minHeight no matter what
|
||||
const ph = topZone.querySelector('.placeholder');
|
||||
if (ph) ph.remove();
|
||||
topZone.classList.remove('highlight');
|
||||
topZone.style.minHeight = '';
|
||||
|
||||
// if no cards left, hide the whole row to remove the gap
|
||||
const hasAny = topZone.querySelectorAll('#uploadCard, #folderManagementCard').length > 0;
|
||||
topZone.style.display = hasAny ? '' : 'none';
|
||||
}
|
||||
|
||||
// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
|
||||
function ensureTopZonePlaceholder() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (!topZone) return;
|
||||
topZone.style.display = '';
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
let placeholder = topZone.querySelector('.placeholder');
|
||||
if (!placeholder) {
|
||||
@@ -578,6 +978,8 @@ function insertCardInHeader(card, event) {
|
||||
border: 'none',
|
||||
padding: '0',
|
||||
boxShadow: 'none',
|
||||
maxWidth: '440px', // NEW: keep card from overflowing center content
|
||||
width: 'max-content' // NEW
|
||||
});
|
||||
document.body.appendChild(modal);
|
||||
modal.addEventListener('mouseover', handleMouseOver);
|
||||
@@ -586,9 +988,10 @@ function insertCardInHeader(card, event) {
|
||||
}
|
||||
if (!modal.contains(card)) {
|
||||
const hiddenContainer = document.getElementById('hiddenCardsContainer');
|
||||
if (hiddenContainer && hiddenContainer.contains(card)) {
|
||||
hiddenContainer.removeChild(card);
|
||||
}
|
||||
if (hiddenContainer && hiddenContainer.contains(card)) hiddenContainer.removeChild(card);
|
||||
// Clear sticky widths before placing in modal
|
||||
card.style.width = '';
|
||||
card.style.minWidth = '';
|
||||
modal.appendChild(card);
|
||||
}
|
||||
modal.style.visibility = 'visible';
|
||||
@@ -638,10 +1041,13 @@ function insertCardInHeader(card, event) {
|
||||
export function initDragAndDrop() {
|
||||
function run() {
|
||||
// make sure toggle exists even if user hasn't dragged yet
|
||||
// ensureSidebarToggle();
|
||||
//applySidebarCollapsed();
|
||||
loadSidebarOrder();
|
||||
loadHeaderOrder();
|
||||
|
||||
// 2) Then paint visibility/toggle
|
||||
applyZonesCollapsed();
|
||||
ensureZonesToggle();
|
||||
ensureZonesToggle();
|
||||
updateZonesToggleUI();
|
||||
|
||||
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
draggableCards.forEach(card => {
|
||||
@@ -672,16 +1078,23 @@ export function initDragAndDrop() {
|
||||
card.classList.add('dragging');
|
||||
card.style.pointerEvents = 'none';
|
||||
addTopZoneHighlight();
|
||||
showTopZoneWhileDragging();
|
||||
|
||||
const sidebar = getSidebar();
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = isSidebarCollapsed() ? 'none' : 'block';
|
||||
sidebar.style.display = isZonesCollapsed() ? 'none' : 'block';
|
||||
sidebar.classList.add('highlight');
|
||||
sidebar.style.height = '800px';
|
||||
sidebar.style.minWidth = '280px';
|
||||
}
|
||||
|
||||
showHeaderDropZone();
|
||||
const topZone = getTopZone();
|
||||
if (topZone) {
|
||||
topZone.style.display = '';
|
||||
ensureTopZonePlaceholder();
|
||||
}
|
||||
|
||||
initialLeft = initialRect.left + window.pageXOffset;
|
||||
initialTop = initialRect.top + window.pageYOffset;
|
||||
@@ -728,12 +1141,13 @@ export function initDragAndDrop() {
|
||||
isDragging = false;
|
||||
card.style.pointerEvents = '';
|
||||
card.classList.remove('dragging');
|
||||
removeTopZoneHighlight();
|
||||
|
||||
|
||||
const sidebar = getSidebar();
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('highlight');
|
||||
sidebar.style.height = '';
|
||||
sidebar.style.minWidth = '';
|
||||
}
|
||||
|
||||
if (card.headerIconButton) {
|
||||
@@ -760,7 +1174,7 @@ export function initDragAndDrop() {
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= dropZoneBottom
|
||||
e.clientY <= rect.bottom
|
||||
) {
|
||||
insertCardInSidebar(card, e);
|
||||
droppedInSidebar = true;
|
||||
@@ -787,6 +1201,7 @@ export function initDragAndDrop() {
|
||||
ensureTopZonePlaceholder();
|
||||
updateTopZoneLayout();
|
||||
container.appendChild(card);
|
||||
card.dataset.originalContainerId = container.id;
|
||||
droppedInTop = true;
|
||||
card.style.width = "363px";
|
||||
animateVerticalSlide(card);
|
||||
@@ -843,6 +1258,11 @@ export function initDragAndDrop() {
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
hideHeaderDropZone();
|
||||
|
||||
cleanupTopZoneAfterDrop();
|
||||
snapshotZoneLocations();
|
||||
const tz = getTopZone();
|
||||
if (tz) tz.style.minHeight = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
2
public/js/version.js
Normal file
2
public/js/version.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// generated by CI
|
||||
window.APP_VERSION = 'v1.6.6';
|
||||
Reference in New Issue
Block a user