release(v1.6.3): drag/drop card persistence, admin UX fixes, and docs (closes #58)

This commit is contained in:
Ryan
2025-10-24 00:22:22 -04:00
committed by GitHub
parent edefaaca36
commit 88a8857a6f
4 changed files with 614 additions and 183 deletions

View File

@@ -1,5 +1,45 @@
# Changelog # Changelog
## 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) ## Changes 10/23/2025 (v1.6.2)
feat(i18n,auth): add Simplified Chinese (zh-CN) and expose in User Panel feat(i18n,auth): add Simplified Chinese (zh-CN) and expose in User Panel

View File

@@ -105,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) ### 1) Running with Docker (Recommended)
#### Pull the image #### Pull the image
@@ -135,6 +151,8 @@ docker run -d \
error311/filerise-docker:latest 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`. This starts FileRise on port **8080** → visit `http://your-server-ip:8080`.
**Notes** **Notes**
@@ -185,6 +203,8 @@ services:
Access at `http://localhost:8080` (or your servers IP). Access at `http://localhost:8080` (or your servers IP).
The example sets a custom `PERSISTENT_TOKENS_KEY`—change it to a strong random string. 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** **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. On first launch, if no users exist, youll be prompted to create an **Admin account**. Then use **User Management** to add more users.
@@ -249,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 ## Unraid
- Install from **Community Apps** → search **FileRise**. - Install from **Community Apps** → search **FileRise**.
@@ -258,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 ## Quick-start: Mount via WebDAV
Once FileRise is running, enable WebDAV in the admin panel. Once FileRise is running, enable WebDAV in the admin panel.
@@ -338,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 ## 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). - **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

@@ -4,10 +4,19 @@ import { loadAdminConfigFunc } from './auth.js';
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
import { sendRequest } from './networkUtils.js'; import { sendRequest } from './networkUtils.js';
const version = "v1.6.2"; const version = "v1.6.3";
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`; 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) === */ /* === BEGIN: Folder Access helpers (merged + improved) === */
function qs(scope, sel){ return (scope||document).querySelector(sel); } function qs(scope, sel){ return (scope||document).querySelector(sel); }
function qsa(scope, sel){ return Array.from((scope||document).querySelectorAll(sel)); } function qsa(scope, sel){ return Array.from((scope||document).querySelectorAll(sel)); }
@@ -194,6 +203,25 @@ async function safeJson(res) {
@media (max-width: 900px) { @media (max-width: 900px) {
.folder-access-list { --col-perm: 72px; --col-folder-min: 240px; } .folder-access-list { --col-perm: 72px; --col-folder-min: 240px; }
} }
/* 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); document.head.appendChild(style);
})(); })();
@@ -617,21 +645,29 @@ export async function closeAdminPanel() {
New: Folder Access (ACL) UI New: Folder Access (ACL) UI
=========================== */ =========================== */
let __allFoldersCache = null; // array of folder strings let __allFoldersCache = null;
async function getAllFolders() {
if (__allFoldersCache) return __allFoldersCache.slice(); async function getAllFolders(force = false) {
const res = await fetch('/api/folder/getFolderList.php', { credentials: 'include' }); if (!force && __allFoldersCache) return __allFoldersCache.slice();
const data = await safeJson(res).catch(() => []);
const list = Array.isArray(data) const res = await fetch('/api/folder/getFolderList.php?ts=' + Date.now(), {
? data.map(x => (typeof x === 'string' ? x : x.folder)).filter(Boolean) credentials: 'include',
: []; cache: 'no-store',
const hidden = new Set(["profile_pics", "trash"]); headers: { 'Cache-Control': 'no-store' }
const cleaned = list });
.filter(f => f && !hidden.has(f.toLowerCase())) const data = await safeJson(res).catch(() => []);
.sort((a, b) => (a === 'root' ? -1 : b === 'root' ? 1 : a.localeCompare(b))); const list = Array.isArray(data)
__allFoldersCache = cleaned; ? data.map(x => (typeof x === 'string' ? x : x.folder)).filter(Boolean)
return cleaned.slice(); : [];
}
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) { async function getUserGrants(username) {
const res = await fetch(`/api/admin/acl/getGrants.php?user=${encodeURIComponent(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); container.appendChild(list);
const headerHtml = ` const headerHtml = `
<div class="folder-access-header"> <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_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('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> <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; const shareFolderDisabled = !g.view;
return ` return `
<div class="folder-access-row" data-folder="${folder}"> <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="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="viewOwn" ${g.viewOwn ? 'checked' : ''}></div>
<div class="perm-col"><input type="checkbox" data-cap="write" ${writeMetaChecked ? '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 () => { document.getElementById("saveUserPermissionsBtn").addEventListener("click", async () => {
const rows = userPermissionsModal.querySelectorAll(".user-permission-row"); const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
const changes = []; const changes = [];
rows.forEach(row => { rows.forEach(row => {
const username = String(row.getAttribute("data-username") || "").trim(); if (row.getAttribute("data-admin") === "1") return; // skip admins
if (!username) return; const username = String(row.getAttribute("data-username") || "").trim();
const grantsBox = row.querySelector(".folder-grants-box"); if (!username) return;
if (!grantsBox || grantsBox.getAttribute('data-loaded') !== '1') return; const grantsBox = row.querySelector(".folder-grants-box");
const grants = collectGrantsFrom(grantsBox); if (!grantsBox || grantsBox.getAttribute('data-loaded') !== '1') return;
changes.push({ user: username, grants }); const grants = collectGrantsFrom(grantsBox);
}); changes.push({ user: username, grants });
});
try { try {
if (changes.length === 0) { showToast(tf("nothing_to_save", "Nothing to save")); return; } if (changes.length === 0) { showToast(tf("nothing_to_save", "Nothing to save")); return; }
await sendRequest("/api/admin/acl/saveGrants.php", "POST", await sendRequest("/api/admin/acl/saveGrants.php", "POST",
@@ -1053,14 +1098,17 @@ async function fetchAllUserFlags() {
function flagRow(u, flags) { function flagRow(u, flags) {
const f = flags[u.username] || {}; const f = flags[u.username] || {};
const isAdmin = String(u.role) === "1" || u.username.toLowerCase() === "admin"; 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 ` return `
<tr data-username="${u.username}"> <tr data-username="${u.username}" ${isAdmin ? "data-admin='1'" : ""}>
<td><strong>${u.username}</strong></td> <td><strong>${u.username}</strong>${note}</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="readOnly" ${f.readOnly ? "checked" : ""} ${disabledAttr}></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="disableUpload" ${f.disableUpload ? "checked" : ""} ${disabledAttr}></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="canShare" ${f.canShare ? "checked" : ""} ${disabledAttr}></td>
<td style="text-align:center;"><input type="checkbox" data-flag="bypassOwnership" ${f.bypassOwnership ? "checked" : ""}></td> <td style="text-align:center;"><input type="checkbox" data-flag="bypassOwnership" ${f.bypassOwnership ? "checked" : ""} ${disabledAttr}></td>
</tr> </tr>
`; `;
} }
@@ -1092,7 +1140,7 @@ export async function openUserFlagsModal() {
<h3>${tf("user_permissions", "User Permissions")}</h3> <h3>${tf("user_permissions", "User Permissions")}</h3>
<p class="muted" style="margin-top:-6px;"> <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> </p>
<div id="userFlagsBody" <div id="userFlagsBody"
@@ -1158,6 +1206,7 @@ async function saveUserFlags() {
const rows = body?.querySelectorAll("tbody tr[data-username]") || []; const rows = body?.querySelectorAll("tbody tr[data-username]") || [];
const permissions = []; const permissions = [];
rows.forEach(tr => { rows.forEach(tr => {
if (tr.getAttribute("data-admin") === "1") return; // don't send admin updates
const username = tr.getAttribute("data-username"); const username = tr.getAttribute("data-username");
const get = k => tr.querySelector(`input[data-flag="${k}"]`).checked; const get = k => tr.querySelector(`input[data-flag="${k}"]`).checked;
permissions.push({ permissions.push({
@@ -1201,61 +1250,73 @@ async function loadUserPermissionsList() {
return; return;
} }
const folders = await getAllFolders(); const folders = await getAllFolders(true);
listContainer.innerHTML = ""; listContainer.innerHTML = "";
users.forEach(user => { users.forEach(user => {
if ((user.role && String(user.role) === "1") || String(user.username).toLowerCase() === "admin") return; const isAdmin = (user.role && String(user.role) === "1") || String(user.username).toLowerCase() === "admin";
const row = document.createElement("div"); const row = document.createElement("div");
row.classList.add("user-permission-row"); row.classList.add("user-permission-row");
row.setAttribute("data-username", user.username); row.setAttribute("data-username", user.username);
row.style.padding = "6px 0"; if (isAdmin) row.setAttribute("data-admin", "1"); // mark admins
row.style.padding = "6px 0";
row.innerHTML = ` row.innerHTML = `
<div class="user-perm-header" tabindex="0" role="button" aria-expanded="false" <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;"> 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> <span class="perm-caret" style="display:inline-block; transform: rotate(-90deg); transition: transform 120ms ease;">▸</span>
<strong>${user.username}</strong> <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>`
</div> : `<span class="muted" style="margin-left:auto;">${tf('click_to_edit', 'Click to edit')}</span>`}
<div class="user-perm-details" style="display:none; margin:8px 0 12px;"> </div>
<div class="folder-grants-box" data-loaded="0"></div> <div class="user-perm-details" style="display:none; margin:8px 0 12px;">
</div> <div class="folder-grants-box" data-loaded="0"></div>
`; </div>
`;
const header = row.querySelector(".user-perm-header"); const header = row.querySelector(".user-perm-header");
const details = row.querySelector(".user-perm-details"); const details = row.querySelector(".user-perm-details");
const caret = row.querySelector(".perm-caret"); const caret = row.querySelector(".perm-caret");
const grantsBox = row.querySelector(".folder-grants-box"); const grantsBox = row.querySelector(".folder-grants-box");
async function ensureLoaded() { async function ensureLoaded() {
if (grantsBox.dataset.loaded === "1") return; if (grantsBox.dataset.loaded === "1") return;
try { try {
const grants = await getUserGrants(user.username); let grants;
renderFolderGrantsUI(user.username, grantsBox, ["root", ...folders.filter(f => f !== "root")], grants); if (isAdmin) {
grantsBox.dataset.loaded = "1"; // synthesize full access
} catch (e) { const ordered = ["root", ...folders.filter(f => f !== "root")];
console.error(e); grants = buildFullGrantsForAllFolders(ordered);
grantsBox.innerHTML = `<div class="muted">${tf("error_loading_user_grants", "Error loading user grants")}</div>`; 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() { function toggleOpen() {
const willShow = details.style.display === "none"; const willShow = details.style.display === "none";
details.style.display = willShow ? "block" : "none"; details.style.display = willShow ? "block" : "none";
header.setAttribute("aria-expanded", willShow ? "true" : "false"); header.setAttribute("aria-expanded", willShow ? "true" : "false");
caret.style.transform = willShow ? "rotate(0deg)" : "rotate(-90deg)"; caret.style.transform = willShow ? "rotate(0deg)" : "rotate(-90deg)";
if (willShow) ensureLoaded(); if (willShow) ensureLoaded();
} }
header.addEventListener("click", toggleOpen); header.addEventListener("click", toggleOpen);
header.addEventListener("keydown", e => { header.addEventListener("keydown", e => {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleOpen(); } if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleOpen(); }
}); });
listContainer.appendChild(row); listContainer.appendChild(row);
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>"; listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";

View File

@@ -8,11 +8,216 @@
const MEDIUM_MIN = 1205; // matches your small-screen cutoff const MEDIUM_MIN = 1205; // matches your small-screen cutoff
const MEDIUM_MAX = 1600; // tweak as you like 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_LEFT_PX = 100;
const TOGGLE_ICON_OPEN = 'view_sidebar'; const TOGGLE_ICON_OPEN = 'view_sidebar';
const TOGGLE_ICON_CLOSED = 'menu'; const TOGGLE_ICON_CLOSED = 'menu';
// Cards we manage
const KNOWN_CARD_IDS = ['uploadCard', 'folderManagementCard'];
const CARD_IDS = ['uploadCard', 'folderManagementCard'];
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 were 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;
}
}
// 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 arent collapsed
if (sidebar) sidebar.style.display = isZonesCollapsed() ? 'none' : 'block';
updateTopZoneLayout();
updateSidebarVisibility();
__lastIsSmall = false;
}
}
function updateSidebarToggleUI() { function updateSidebarToggleUI() {
const btn = document.getElementById('sidebarToggleFloating'); const btn = document.getElementById('sidebarToggleFloating');
@@ -22,9 +227,8 @@ function updateSidebarToggleUI() {
if (!hasSidebarCards()) { btn.remove(); return; } if (!hasSidebarCards()) { btn.remove(); return; }
const collapsed = isSidebarCollapsed(); const collapsed = isSidebarCollapsed();
btn.innerHTML = `<i class="material-icons" aria-hidden="true">${ btn.innerHTML = `<i class="material-icons" aria-hidden="true">${collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN
collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN }</i>`;
}</i>`;
btn.title = collapsed ? 'Show sidebar' : 'Hide sidebar'; btn.title = collapsed ? 'Show sidebar' : 'Hide sidebar';
btn.style.display = 'block'; btn.style.display = 'block';
btn.classList.toggle('toggle-ping', collapsed); btn.classList.toggle('toggle-ping', collapsed);
@@ -43,28 +247,45 @@ function hasTopZoneCards() {
// Both cards are in the top zone (upload + folder) // Both cards are in the top zone (upload + folder)
function allCardsInTopZone() { function allCardsInTopZone() {
const tz = getTopZone(); const tz = getTopZone();
if (!tz) return false; if (!tz) return false;
const hasUpload = !!tz.querySelector('#uploadCard'); const hasUpload = !!tz.querySelector('#uploadCard');
const hasFolder = !!tz.querySelector('#folderManagementCard'); const hasFolder = !!tz.querySelector('#folderManagementCard');
return hasUpload && hasFolder; return hasUpload && hasFolder;
} }
function isZonesCollapsed() { function isZonesCollapsed() {
return localStorage.getItem('zonesCollapsed') === '1'; return localStorage.getItem('zonesCollapsed') === '1';
} }
function setZonesCollapsed(collapsed) { function setZonesCollapsed(collapsed) {
localStorage.setItem('zonesCollapsed', collapsed ? '1' : '0'); 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(); updateZonesToggleUI();
} }
function applyZonesCollapsed() { function applyZonesCollapsed() {
const collapsed = isZonesCollapsed(); const collapsed = isZonesCollapsed();
const sidebar = getSidebar(); const sidebar = getSidebar();
const topZone = getTopZone(); const topZone = getTopZone();
if (sidebar) sidebar.style.display = collapsed ? 'none' : (hasSidebarCards() ? 'block' : 'none'); 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() { function isMediumScreen() {
@@ -101,14 +322,7 @@ function applySidebarCollapsed() {
} }
function ensureZonesToggle() { function ensureZonesToggle() {
// show only if at least one zone *can* show a card
const shouldShow = hasSidebarCards() || hasTopZoneCards();
let btn = document.getElementById('sidebarToggleFloating'); let btn = document.getElementById('sidebarToggleFloating');
if (!shouldShow) {
if (btn) btn.remove();
return;
}
if (!btn) { if (!btn) {
btn = document.createElement('button'); btn = document.createElement('button');
btn.id = 'sidebarToggleFloating'; btn.id = 'sidebarToggleFloating';
@@ -126,11 +340,11 @@ function ensureZonesToggle() {
background: '#fff', background: '#fff',
cursor: 'pointer', cursor: 'pointer',
boxShadow: '0 2px 6px rgba(0,0,0,.15)', boxShadow: '0 2px 6px rgba(0,0,0,.15)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: '0', padding: '0',
lineHeight: '0', lineHeight: '0',
}); });
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
setZonesCollapsed(!isZonesCollapsed()); setZonesCollapsed(!isZonesCollapsed());
@@ -139,23 +353,18 @@ function ensureZonesToggle() {
} }
updateZonesToggleUI(); updateZonesToggleUI();
} }
function updateZonesToggleUI() { function updateZonesToggleUI() {
const btn = document.getElementById('sidebarToggleFloating'); const btn = document.getElementById('sidebarToggleFloating');
if (!btn) return; if (!btn) return;
// if neither zone has cards, remove the toggle // Never remove the button just because cards are in header.
if (!hasSidebarCards() && !hasTopZoneCards()) {
btn.remove();
return;
}
const collapsed = isZonesCollapsed(); const collapsed = isZonesCollapsed();
const iconName = collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN; const iconName = collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN;
btn.innerHTML = `<i class="material-icons toggle-icon" aria-hidden="true">${iconName}</i>`; btn.innerHTML = `<i class="material-icons toggle-icon" aria-hidden="true">${iconName}</i>`;
btn.title = collapsed ? 'Show panels' : 'Hide panels'; btn.title = collapsed ? 'Show panels' : 'Hide panels';
btn.style.display = 'block'; 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'); const iconEl = btn.querySelector('.toggle-icon');
if (iconEl) { if (iconEl) {
iconEl.style.transition = 'transform 0.2s ease'; iconEl.style.transition = 'transform 0.2s ease';
@@ -228,27 +437,35 @@ export function loadSidebarOrder() {
const headerOrderStr = localStorage.getItem('headerOrder'); const headerOrderStr = localStorage.getItem('headerOrder');
const defaultAppliedKey = 'layoutDefaultApplied_v1'; // bump if logic changes const defaultAppliedKey = 'layoutDefaultApplied_v1'; // bump if logic changes
// If we have a saved order (sidebar), honor it as before
if (orderStr) { // One-time default: if no saved order and no header order,
const order = JSON.parse(orderStr || '[]'); // put cards into the sidebar on all ≥ MEDIUM_MIN screens.
if (Array.isArray(order) && order.length > 0) { if ((!orderStr || !JSON.parse(orderStr || '[]').length) &&
const mainWrapper = document.querySelector('.main-wrapper'); (!headerOrderStr || !JSON.parse(headerOrderStr || '[]').length)) {
if (mainWrapper) mainWrapper.style.display = 'flex';
order.forEach(id => { const isLargeEnough = window.innerWidth >= MEDIUM_MIN;
const card = document.getElementById(id); if (isLargeEnough) {
if (card && card.parentNode?.id !== 'sidebarDropArea') { const mainWrapper = document.querySelector('.main-wrapper');
sidebar.appendChild(card); if (mainWrapper) mainWrapper.style.display = 'flex';
animateVerticalSlide(card);
} const moved = [];
}); ['uploadCard', 'folderManagementCard'].forEach(id => {
updateSidebarVisibility(); const card = document.getElementById(id);
//applySidebarCollapsed(); // NEW: honor collapsed state if (card && card.parentNode?.id !== 'sidebarDropArea') {
//ensureSidebarToggle(); // NEW: inject toggle // clear any sticky widths from header/top
applyZonesCollapsed(); card.style.width = '';
ensureZonesToggle(); card.style.minWidth = '';
getSidebar().appendChild(card);
return; animateVerticalSlide(card);
} moved.push(id);
}
});
if (moved.length) {
localStorage.setItem('sidebarOrder', JSON.stringify(moved));
}
}
} }
// No sidebar order saved yet: if user has header icons saved, do nothing (they've customized) // No sidebar order saved yet: if user has header icons saved, do nothing (they've customized)
@@ -258,7 +475,7 @@ export function loadSidebarOrder() {
//applySidebarCollapsed(); //applySidebarCollapsed();
//ensureSidebarToggle(); //ensureSidebarToggle();
applyZonesCollapsed(); applyZonesCollapsed();
ensureZonesToggle(); ensureZonesToggle();
return; return;
} }
@@ -289,34 +506,28 @@ export function loadSidebarOrder() {
//applySidebarCollapsed(); //applySidebarCollapsed();
//ensureSidebarToggle(); //ensureSidebarToggle();
applyZonesCollapsed(); applyZonesCollapsed();
ensureZonesToggle(); ensureZonesToggle();
} }
export function loadHeaderOrder() { export function loadHeaderOrder() {
const headerDropArea = document.getElementById('headerDropArea'); const headerDropArea = document.getElementById('headerDropArea');
if (!headerDropArea) return; if (!headerDropArea) return;
// 1) Clear out any icons that might already be in the drop area // If panels are expanded, do not re-create header icons.
headerDropArea.innerHTML = ''; if (!isZonesCollapsed()) {
headerDropArea.innerHTML = '';
// 2) Read the saved array (or empty array if invalid/missing) localStorage.removeItem('headerOrder');
let stored; return;
try {
stored = JSON.parse(localStorage.getItem('headerOrder') || '[]');
} catch {
stored = [];
} }
// 3) Deduplicate IDs headerDropArea.innerHTML = '';
let stored;
try { stored = JSON.parse(localStorage.getItem('headerOrder') || '[]'); } catch { stored = []; }
const uniqueIds = Array.from(new Set(stored)); const uniqueIds = Array.from(new Set(stored));
// 4) Re-insert exactly one icon per saved card ID
uniqueIds.forEach(id => { uniqueIds.forEach(id => {
const card = document.getElementById(id); const card = document.getElementById(id);
if (card) insertCardInHeader(card, null); if (card) insertCardInHeader(card, null);
}); });
// 5) Persist the cleaned, deduped list back to storage
localStorage.setItem('headerOrder', JSON.stringify(uniqueIds)); localStorage.setItem('headerOrder', JSON.stringify(uniqueIds));
} }
@@ -332,13 +543,13 @@ function updateSidebarVisibility() {
sidebar.style.height = ''; sidebar.style.height = '';
if (anyCards) { if (anyCards) {
sidebar.classList.add('active'); sidebar.classList.add('active');
// respect the unified zones-collapsed switch // respect the unified zones-collapsed switch
sidebar.style.display = isZonesCollapsed() ? 'none' : 'block'; sidebar.style.display = isZonesCollapsed() ? 'none' : 'block';
} else { } else {
sidebar.classList.remove('active'); sidebar.classList.remove('active');
sidebar.style.display = 'none'; sidebar.style.display = 'none';
} }
// Save order and update toggle visibility // Save order and update toggle visibility
saveSidebarOrder(); saveSidebarOrder();
@@ -358,19 +569,22 @@ function saveHeaderOrder() {
// Internal helper: update top zone layout (center a card if one column is empty). // Internal helper: update top zone layout (center a card if one column is empty).
function updateTopZoneLayout() { function updateTopZoneLayout() {
const topZone = getTopZone();
const leftCol = document.getElementById('leftCol'); const leftCol = document.getElementById('leftCol');
const rightCol = document.getElementById('rightCol'); const rightCol = document.getElementById('rightCol');
const leftIsEmpty = !leftCol?.querySelector('#uploadCard'); const hasUpload = !!topZone?.querySelector('#uploadCard');
const rightIsEmpty = !rightCol?.querySelector('#folderManagementCard'); const hasFolder = !!topZone?.querySelector('#folderManagementCard');
if (leftCol && rightCol) { if (leftCol && rightCol) {
if (leftIsEmpty && !rightIsEmpty) { if (hasUpload && !hasFolder) {
leftCol.style.display = 'none';
rightCol.style.margin = '0 auto';
} else if (rightIsEmpty && !leftIsEmpty) {
rightCol.style.display = 'none'; rightCol.style.display = 'none';
leftCol.style.margin = '0 auto'; 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 { } else {
leftCol.style.display = ''; leftCol.style.display = '';
rightCol.style.display = ''; rightCol.style.display = '';
@@ -378,6 +592,9 @@ function updateTopZoneLayout() {
rightCol.style.margin = ''; 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. // When a card is being dragged, if the top drop zone is empty, set its min-height.
@@ -422,6 +639,7 @@ function animateVerticalSlide(card) {
function insertCardInSidebar(card, event) { function insertCardInSidebar(card, event) {
const sidebar = getSidebar(); const sidebar = getSidebar();
if (!sidebar) return; if (!sidebar) return;
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard')); const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
let inserted = false; let inserted = false;
for (const currentCard of existingCards) { for (const currentCard of existingCards) {
@@ -433,14 +651,19 @@ function insertCardInSidebar(card, event) {
break; break;
} }
} }
if (!inserted) { if (!inserted) sidebar.appendChild(card);
sidebar.appendChild(card);
} // Make it fill the sidebar and clear any sticky width from header/top zone.
// Ensure card fills the sidebar.
card.style.width = '100%'; card.style.width = '100%';
removeHeaderIconForCard(card); // NEW: remove any header artifacts
card.dataset.originalContainerId = 'sidebarDropArea';
animateVerticalSlide(card); 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. // Internal helper: save the current sidebar card order to localStorage.
@@ -472,16 +695,55 @@ function moveSidebarCardsToTop() {
} }
// Listen for window resize to automatically move sidebar cards back to top on small screens. // Listen for window resize to automatically move sidebar cards back to top on small screens.
window.addEventListener('resize', function () { (function () {
if (window.innerWidth < 1205) { let rAF = null;
moveSidebarCardsToTop(); 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. // This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
function ensureTopZonePlaceholder() { function ensureTopZonePlaceholder() {
const topZone = document.getElementById('uploadFolderRow'); const topZone = document.getElementById('uploadFolderRow');
if (!topZone) return; if (!topZone) return;
topZone.style.display = '';
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) { if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
let placeholder = topZone.querySelector('.placeholder'); let placeholder = topZone.querySelector('.placeholder');
if (!placeholder) { if (!placeholder) {
@@ -578,6 +840,8 @@ function insertCardInHeader(card, event) {
border: 'none', border: 'none',
padding: '0', padding: '0',
boxShadow: 'none', boxShadow: 'none',
maxWidth: '440px', // NEW: keep card from overflowing center content
width: 'max-content' // NEW
}); });
document.body.appendChild(modal); document.body.appendChild(modal);
modal.addEventListener('mouseover', handleMouseOver); modal.addEventListener('mouseover', handleMouseOver);
@@ -586,9 +850,10 @@ function insertCardInHeader(card, event) {
} }
if (!modal.contains(card)) { if (!modal.contains(card)) {
const hiddenContainer = document.getElementById('hiddenCardsContainer'); const hiddenContainer = document.getElementById('hiddenCardsContainer');
if (hiddenContainer && hiddenContainer.contains(card)) { if (hiddenContainer && hiddenContainer.contains(card)) hiddenContainer.removeChild(card);
hiddenContainer.removeChild(card); // Clear sticky widths before placing in modal
} card.style.width = '';
card.style.minWidth = '';
modal.appendChild(card); modal.appendChild(card);
} }
modal.style.visibility = 'visible'; modal.style.visibility = 'visible';
@@ -638,10 +903,13 @@ function insertCardInHeader(card, event) {
export function initDragAndDrop() { export function initDragAndDrop() {
function run() { function run() {
// make sure toggle exists even if user hasn't dragged yet // make sure toggle exists even if user hasn't dragged yet
// ensureSidebarToggle(); loadSidebarOrder();
//applySidebarCollapsed(); loadHeaderOrder();
// 2) Then paint visibility/toggle
applyZonesCollapsed(); applyZonesCollapsed();
ensureZonesToggle(); ensureZonesToggle();
updateZonesToggleUI();
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard'); const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
draggableCards.forEach(card => { draggableCards.forEach(card => {
@@ -672,16 +940,24 @@ export function initDragAndDrop() {
card.classList.add('dragging'); card.classList.add('dragging');
card.style.pointerEvents = 'none'; card.style.pointerEvents = 'none';
addTopZoneHighlight(); addTopZoneHighlight();
showTopZoneWhileDragging();
const sidebar = getSidebar(); const sidebar = getSidebar();
if (sidebar) { if (sidebar) {
sidebar.classList.add('active'); sidebar.classList.add('active');
sidebar.style.display = isSidebarCollapsed() ? 'none' : 'block'; sidebar.style.display = isZonesCollapsed() ? 'none' : 'block';
sidebar.classList.add('highlight'); sidebar.classList.add('highlight');
sidebar.style.height = '800px'; sidebar.style.height = '800px';
sidebar.style.minWidth = '280px';
} }
showHeaderDropZone(); showHeaderDropZone();
const topZone = getTopZone();
if (topZone)
{
topZone.style.display = '';
ensureTopZonePlaceholder();
}
initialLeft = initialRect.left + window.pageXOffset; initialLeft = initialRect.left + window.pageXOffset;
initialTop = initialRect.top + window.pageYOffset; initialTop = initialRect.top + window.pageYOffset;
@@ -728,12 +1004,13 @@ export function initDragAndDrop() {
isDragging = false; isDragging = false;
card.style.pointerEvents = ''; card.style.pointerEvents = '';
card.classList.remove('dragging'); card.classList.remove('dragging');
removeTopZoneHighlight();
const sidebar = getSidebar(); const sidebar = getSidebar();
if (sidebar) { if (sidebar) {
sidebar.classList.remove('highlight'); sidebar.classList.remove('highlight');
sidebar.style.height = ''; sidebar.style.height = '';
sidebar.style.minWidth = '';
} }
if (card.headerIconButton) { if (card.headerIconButton) {
@@ -760,7 +1037,7 @@ export function initDragAndDrop() {
e.clientX >= rect.left && e.clientX >= rect.left &&
e.clientX <= rect.right && e.clientX <= rect.right &&
e.clientY >= rect.top && e.clientY >= rect.top &&
e.clientY <= dropZoneBottom e.clientY <= rect.bottom
) { ) {
insertCardInSidebar(card, e); insertCardInSidebar(card, e);
droppedInSidebar = true; droppedInSidebar = true;
@@ -787,6 +1064,7 @@ export function initDragAndDrop() {
ensureTopZonePlaceholder(); ensureTopZonePlaceholder();
updateTopZoneLayout(); updateTopZoneLayout();
container.appendChild(card); container.appendChild(card);
card.dataset.originalContainerId = container.id;
droppedInTop = true; droppedInTop = true;
card.style.width = "363px"; card.style.width = "363px";
animateVerticalSlide(card); animateVerticalSlide(card);
@@ -843,6 +1121,10 @@ export function initDragAndDrop() {
updateTopZoneLayout(); updateTopZoneLayout();
updateSidebarVisibility(); updateSidebarVisibility();
hideHeaderDropZone(); hideHeaderDropZone();
cleanupTopZoneAfterDrop();
const tz = getTopZone();
if (tz) tz.style.minHeight = '';
} }
}); });
}); });