Organize app source
This commit is contained in:
414
js/auth.js
Normal file
414
js/auth.js
Normal file
@@ -0,0 +1,414 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmModal } from './domUtils.js';
|
||||
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import {
|
||||
openTOTPLoginModal,
|
||||
openUserPanel,
|
||||
openTOTPModal,
|
||||
closeTOTPModal,
|
||||
openAdminPanel,
|
||||
closeAdminPanel,
|
||||
setLastLoginData
|
||||
} from './authModals.js';
|
||||
|
||||
// Production OIDC configuration (override via API as needed)
|
||||
const currentOIDCConfig = {
|
||||
providerUrl: "https://your-oidc-provider.com",
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET",
|
||||
redirectUri: "https://yourdomain.com/auth.php?oidc=callback",
|
||||
globalOtpauthUrl: ""
|
||||
};
|
||||
window.currentOIDCConfig = currentOIDCConfig;
|
||||
|
||||
/* ----------------- Utility Functions ----------------- */
|
||||
function updateItemsPerPageSelect() {
|
||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||
if (selectElem) {
|
||||
selectElem.value = localStorage.getItem("itemsPerPage") || "10";
|
||||
}
|
||||
}
|
||||
|
||||
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
||||
const authForm = document.getElementById("authForm");
|
||||
if (authForm) authForm.style.display = disableFormLogin ? "none" : "block";
|
||||
const basicAuthLink = document.querySelector("a[href='login_basic.php']");
|
||||
if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
||||
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||
if (oidcLoginBtn) oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block";
|
||||
}
|
||||
|
||||
function updateLoginOptionsUIFromStorage() {
|
||||
updateLoginOptionsUI({
|
||||
disableFormLogin: localStorage.getItem("disableFormLogin") === "true",
|
||||
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
||||
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
|
||||
});
|
||||
}
|
||||
|
||||
function loadAdminConfigFunc() {
|
||||
return fetch("getConfig.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin);
|
||||
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
|
||||
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
|
||||
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/FileRise?issuer=FileRise");
|
||||
updateLoginOptionsUIFromStorage();
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.setItem("disableFormLogin", "false");
|
||||
localStorage.setItem("disableBasicAuth", "false");
|
||||
localStorage.setItem("disableOIDCLogin", "false");
|
||||
localStorage.setItem("globalOtpauthUrl", "otpauth://totp/FileRise?issuer=FileRise");
|
||||
updateLoginOptionsUIFromStorage();
|
||||
});
|
||||
}
|
||||
|
||||
function insertAfter(newNode, referenceNode) {
|
||||
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
|
||||
}
|
||||
|
||||
function updateAuthenticatedUI(data) {
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", true);
|
||||
toggleVisibility("uploadFileForm", true);
|
||||
toggleVisibility("fileListContainer", true);
|
||||
attachEnterKeyListener("addUserModal", "saveUserBtn");
|
||||
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
|
||||
if (typeof data.totp_enabled !== "undefined") {
|
||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||
}
|
||||
|
||||
if (data.username) {
|
||||
localStorage.setItem("username", data.username);
|
||||
}
|
||||
if (typeof data.folderOnly !== "undefined") {
|
||||
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
|
||||
}
|
||||
|
||||
const headerButtons = document.querySelector(".header-buttons");
|
||||
const firstButton = headerButtons.firstElementChild;
|
||||
|
||||
if (data.isAdmin) {
|
||||
let restoreBtn = document.getElementById("restoreFilesBtn");
|
||||
if (!restoreBtn) {
|
||||
restoreBtn = document.createElement("button");
|
||||
restoreBtn.id = "restoreFilesBtn";
|
||||
restoreBtn.classList.add("btn", "btn-warning");
|
||||
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
||||
if (firstButton) {
|
||||
insertAfter(restoreBtn, firstButton);
|
||||
} else {
|
||||
headerButtons.appendChild(restoreBtn);
|
||||
}
|
||||
}
|
||||
restoreBtn.style.display = "block";
|
||||
|
||||
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||
if (!adminPanelBtn) {
|
||||
adminPanelBtn = document.createElement("button");
|
||||
adminPanelBtn.id = "adminPanelBtn";
|
||||
adminPanelBtn.classList.add("btn", "btn-info");
|
||||
adminPanelBtn.innerHTML = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
|
||||
insertAfter(adminPanelBtn, restoreBtn);
|
||||
adminPanelBtn.addEventListener("click", openAdminPanel);
|
||||
} else {
|
||||
adminPanelBtn.style.display = "block";
|
||||
}
|
||||
} else {
|
||||
const restoreBtn = document.getElementById("restoreFilesBtn");
|
||||
if (restoreBtn) restoreBtn.style.display = "none";
|
||||
const adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||
if (adminPanelBtn) adminPanelBtn.style.display = "none";
|
||||
}
|
||||
|
||||
if (window.location.hostname !== "demo.filerise.net") {
|
||||
let userPanelBtn = document.getElementById("userPanelBtn");
|
||||
if (!userPanelBtn) {
|
||||
userPanelBtn = document.createElement("button");
|
||||
userPanelBtn.id = "userPanelBtn";
|
||||
userPanelBtn.classList.add("btn", "btn-user");
|
||||
userPanelBtn.innerHTML = '<i class="material-icons" title="User Panel">account_circle</i>';
|
||||
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||
if (adminPanelBtn) {
|
||||
insertAfter(userPanelBtn, adminPanelBtn);
|
||||
} else {
|
||||
const firstButton = headerButtons.firstElementChild;
|
||||
if (firstButton) {
|
||||
insertAfter(userPanelBtn, firstButton);
|
||||
} else {
|
||||
headerButtons.appendChild(userPanelBtn);
|
||||
}
|
||||
}
|
||||
userPanelBtn.addEventListener("click", openUserPanel);
|
||||
} else {
|
||||
userPanelBtn.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
updateItemsPerPageSelect();
|
||||
updateLoginOptionsUIFromStorage();
|
||||
}
|
||||
|
||||
function checkAuthentication(showLoginToast = true) {
|
||||
return sendRequest("checkAuth.php")
|
||||
.then(data => {
|
||||
if (data.setup) {
|
||||
window.setupMode = true;
|
||||
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
return false;
|
||||
}
|
||||
window.setupMode = false;
|
||||
if (data.authenticated) {
|
||||
if (typeof data.totp_enabled !== "undefined") {
|
||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||
}
|
||||
updateAuthenticatedUI(data);
|
||||
return data;
|
||||
} else {
|
||||
if (showLoginToast) showToast("Please log in to continue.");
|
||||
toggleVisibility("loginForm", true);
|
||||
toggleVisibility("mainOperations", false);
|
||||
toggleVisibility("uploadFileForm", false);
|
||||
toggleVisibility("fileListContainer", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
/* ----------------- Authentication Submission ----------------- */
|
||||
function submitLogin(data) {
|
||||
setLastLoginData(data);
|
||||
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
|
||||
window.location.reload();
|
||||
} else if (response.totp_required) {
|
||||
openTOTPLoginModal();
|
||||
} else if (response.error && response.error.includes("Too many failed login attempts")) {
|
||||
showToast(response.error);
|
||||
const loginButton = document.getElementById("authForm").querySelector("button[type='submit']");
|
||||
if (loginButton) {
|
||||
loginButton.disabled = true;
|
||||
setTimeout(() => {
|
||||
loginButton.disabled = false;
|
||||
showToast("You can now try logging in again.");
|
||||
}, 30 * 60 * 1000);
|
||||
}
|
||||
} else {
|
||||
showToast("Login failed: " + (response.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("Login failed: Unknown error");
|
||||
});
|
||||
}
|
||||
window.submitLogin = submitLogin;
|
||||
|
||||
/* ----------------- Other Helpers and Initialization ----------------- */
|
||||
window.changeItemsPerPage = function (value) {
|
||||
localStorage.setItem("itemsPerPage", value);
|
||||
if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root");
|
||||
};
|
||||
|
||||
function resetUserForm() {
|
||||
document.getElementById("newUsername").value = "";
|
||||
document.getElementById("addUserPassword").value = "";
|
||||
}
|
||||
|
||||
function closeAddUserModal() {
|
||||
toggleVisibility("addUserModal", false);
|
||||
resetUserForm();
|
||||
}
|
||||
|
||||
function closeRemoveUserModal() {
|
||||
toggleVisibility("removeUserModal", false);
|
||||
document.getElementById("removeUsernameSelect").innerHTML = "";
|
||||
}
|
||||
|
||||
function loadUserList() {
|
||||
fetch("getUsers.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const users = Array.isArray(data) ? data : (data.users || []);
|
||||
const selectElem = document.getElementById("removeUsernameSelect");
|
||||
selectElem.innerHTML = "";
|
||||
users.forEach(user => {
|
||||
const option = document.createElement("option");
|
||||
option.value = user.username;
|
||||
option.textContent = user.username;
|
||||
selectElem.appendChild(option);
|
||||
});
|
||||
if (selectElem.options.length === 0) {
|
||||
showToast("No other users found to remove.");
|
||||
closeRemoveUserModal();
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
}
|
||||
window.loadUserList = loadUserList;
|
||||
|
||||
function initAuth() {
|
||||
checkAuthentication(false);
|
||||
loadAdminConfigFunc();
|
||||
const authForm = document.getElementById("authForm");
|
||||
if (authForm) {
|
||||
authForm.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
const rememberMe = document.getElementById("rememberMeCheckbox")
|
||||
? document.getElementById("rememberMeCheckbox").checked
|
||||
: false;
|
||||
const formData = {
|
||||
username: document.getElementById("loginUsername").value.trim(),
|
||||
password: document.getElementById("loginPassword").value.trim(),
|
||||
remember_me: rememberMe
|
||||
};
|
||||
submitLogin(formData);
|
||||
});
|
||||
}
|
||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
||||
fetch("logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
}).then(() => window.location.reload(true)).catch(() => { });
|
||||
});
|
||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||
resetUserForm();
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
||||
const newUsername = document.getElementById("newUsername").value.trim();
|
||||
const newPassword = document.getElementById("addUserPassword").value.trim();
|
||||
const isAdmin = document.getElementById("isAdmin").checked;
|
||||
if (!newUsername || !newPassword) {
|
||||
showToast("Username and password are required!");
|
||||
return;
|
||||
}
|
||||
let url = "addUser.php";
|
||||
if (window.setupMode) url += "?setup=1";
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("User added successfully!");
|
||||
closeAddUserModal();
|
||||
checkAuthentication(false);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not add user"));
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
});
|
||||
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
||||
|
||||
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
||||
loadUserList();
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
document.getElementById("deleteUserBtn").addEventListener("click", async function () {
|
||||
const selectElem = document.getElementById("removeUsernameSelect");
|
||||
const usernameToRemove = selectElem.value;
|
||||
if (!usernameToRemove) {
|
||||
showToast("Please select a user to remove.");
|
||||
return;
|
||||
}
|
||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||
if (!confirmed) return;
|
||||
fetch("removeUser.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
body: JSON.stringify({ username: usernameToRemove })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("User removed successfully!");
|
||||
closeRemoveUserModal();
|
||||
loadUserList();
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not remove user"));
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
});
|
||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
||||
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
||||
document.getElementById("changePasswordModal").style.display = "block";
|
||||
document.getElementById("oldPassword").focus();
|
||||
});
|
||||
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
|
||||
document.getElementById("changePasswordModal").style.display = "none";
|
||||
});
|
||||
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
|
||||
const oldPassword = document.getElementById("oldPassword").value.trim();
|
||||
const newPassword = document.getElementById("newPassword").value.trim();
|
||||
const confirmPassword = document.getElementById("confirmPassword").value.trim();
|
||||
if (!oldPassword || !newPassword || !confirmPassword) {
|
||||
showToast("Please fill in all fields.");
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast("New passwords do not match.");
|
||||
return;
|
||||
}
|
||||
const data = { oldPassword, newPassword, confirmPassword };
|
||||
fetch("changePassword.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
showToast(result.success);
|
||||
document.getElementById("oldPassword").value = "";
|
||||
document.getElementById("newPassword").value = "";
|
||||
document.getElementById("confirmPassword").value = "";
|
||||
document.getElementById("changePasswordModal").style.display = "none";
|
||||
} else {
|
||||
showToast("Error: " + (result.error || "Could not change password."));
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error changing password."); });
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
updateItemsPerPageSelect();
|
||||
updateLoginOptionsUI({
|
||||
disableFormLogin: localStorage.getItem("disableFormLogin") === "true",
|
||||
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
||||
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
|
||||
});
|
||||
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||
if (oidcLoginBtn) {
|
||||
oidcLoginBtn.addEventListener("click", () => {
|
||||
// Redirect to the OIDC auth endpoint. The endpoint can be adjusted if needed.
|
||||
window.location.href = "auth.php?oidc=initiate";
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export { initAuth, checkAuthentication };
|
||||
655
js/authModals.js
Normal file
655
js/authModals.js
Normal file
@@ -0,0 +1,655 @@
|
||||
import { showToast, toggleVisibility } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
|
||||
const version = "v1.0.7";
|
||||
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||
let lastLoginData = null;
|
||||
|
||||
export function setLastLoginData(data) {
|
||||
lastLoginData = data;
|
||||
}
|
||||
|
||||
export function openTOTPLoginModal() {
|
||||
let totpLoginModal = document.getElementById("totpLoginModal");
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const modalBg = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
const textColor = isDarkMode ? "#e0e0e0" : "#000";
|
||||
|
||||
if (!totpLoginModal) {
|
||||
totpLoginModal = document.createElement("div");
|
||||
totpLoginModal.id = "totpLoginModal";
|
||||
totpLoginModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3200;
|
||||
`;
|
||||
totpLoginModal.innerHTML = `
|
||||
<div style="background: ${modalBg}; padding: 20px; border-radius: 8px; text-align: center; position: relative; color: ${textColor};">
|
||||
<span id="closeTOTPLoginModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>Enter TOTP Code</h3>
|
||||
<input type="text" id="totpLoginInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(totpLoginModal);
|
||||
document.getElementById("closeTOTPLoginModal").addEventListener("click", () => {
|
||||
totpLoginModal.style.display = "none";
|
||||
});
|
||||
const totpInput = document.getElementById("totpLoginInput");
|
||||
totpInput.focus();
|
||||
totpInput.addEventListener("input", function () {
|
||||
if (this.value.trim().length === 6 && lastLoginData) {
|
||||
lastLoginData.totp_code = this.value.trim();
|
||||
totpLoginModal.style.display = "none";
|
||||
if (typeof window.submitLogin === "function") {
|
||||
window.submitLogin(lastLoginData);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
totpLoginModal.style.display = "flex";
|
||||
const modalContent = totpLoginModal.firstElementChild;
|
||||
modalContent.style.background = modalBg;
|
||||
modalContent.style.color = textColor;
|
||||
}
|
||||
}
|
||||
|
||||
export function openUserPanel() {
|
||||
const username = localStorage.getItem("username") || "User";
|
||||
let userPanelModal = document.getElementById("userPanelModal");
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const modalContentStyles = `
|
||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
max-height: 90vh;
|
||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||
transform: none;
|
||||
transition: none;
|
||||
`;
|
||||
if (!userPanelModal) {
|
||||
userPanelModal = document.createElement("div");
|
||||
userPanelModal.id = "userPanelModal";
|
||||
userPanelModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
`;
|
||||
userPanelModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>User Panel (${username})</h3>
|
||||
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">Change Password</button>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>TOTP Settings</legend>
|
||||
<div class="form-group">
|
||||
<label for="userTOTPEnabled">Enable TOTP:</label>
|
||||
<input type="checkbox" id="userTOTPEnabled" style="vertical-align: middle;" />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(userPanelModal);
|
||||
document.getElementById("closeUserPanel").addEventListener("click", () => {
|
||||
userPanelModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("openChangePasswordModalBtn").addEventListener("click", () => {
|
||||
document.getElementById("changePasswordModal").style.display = "block";
|
||||
});
|
||||
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
||||
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
|
||||
totpCheckbox.addEventListener("change", function () {
|
||||
localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false");
|
||||
const enabled = this.checked;
|
||||
fetch("updateUserPanel.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ totp_enabled: enabled })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (!result.success) {
|
||||
showToast("Error updating TOTP setting: " + result.error);
|
||||
} else if (enabled) {
|
||||
openTOTPModal();
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error updating TOTP setting."); });
|
||||
});
|
||||
} else {
|
||||
userPanelModal.style.backgroundColor = overlayBackground;
|
||||
const modalContent = userPanelModal.querySelector(".modal-content");
|
||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||
modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc";
|
||||
}
|
||||
userPanelModal.style.display = "flex";
|
||||
}
|
||||
|
||||
export function openTOTPModal() {
|
||||
let totpModal = document.getElementById("totpModal");
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const modalContentStyles = `
|
||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||
padding: 20px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
`;
|
||||
if (!totpModal) {
|
||||
totpModal = document.createElement("div");
|
||||
totpModal.id = "totpModal";
|
||||
totpModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3100;
|
||||
`;
|
||||
totpModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>TOTP Setup</h3>
|
||||
<p>Scan this QR code with your authenticator app:</p>
|
||||
<img src="totp_setup.php?csrf=${encodeURIComponent(window.csrfToken)}" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
<br/>
|
||||
<p>Enter the 6-digit code from your app to confirm setup:</p>
|
||||
<input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
|
||||
<br/><br/>
|
||||
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">Confirm</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(totpModal);
|
||||
// Bind the X button to call closeTOTPModal with disable=true
|
||||
document.getElementById("closeTOTPModal").addEventListener("click", () => {
|
||||
closeTOTPModal(true);
|
||||
});
|
||||
|
||||
// Add event listener for TOTP confirmation
|
||||
document.getElementById("confirmTOTPBtn").addEventListener("click", function () {
|
||||
const code = document.getElementById("totpConfirmInput").value.trim();
|
||||
if (code.length !== 6) {
|
||||
showToast("Please enter a valid 6-digit code.");
|
||||
return;
|
||||
}
|
||||
// Call the endpoint to verify the TOTP code
|
||||
fetch("totp_verify.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ totp_code: code })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
showToast("TOTP successfully enabled.");
|
||||
// On success, close the modal without disabling
|
||||
closeTOTPModal(false);
|
||||
} else {
|
||||
showToast("TOTP verification failed: " + (result.error || "Invalid code."));
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error verifying TOTP code."); });
|
||||
});
|
||||
} else {
|
||||
totpModal.style.display = "flex";
|
||||
totpModal.style.backgroundColor = overlayBackground;
|
||||
const modalContent = totpModal.querySelector(".modal-content");
|
||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||
}
|
||||
}
|
||||
|
||||
// Updated closeTOTPModal function with a disable parameter
|
||||
export function closeTOTPModal(disable = true) {
|
||||
const totpModal = document.getElementById("totpModal");
|
||||
if (totpModal) totpModal.style.display = "none";
|
||||
|
||||
if (disable) {
|
||||
// Uncheck the Enable TOTP checkbox
|
||||
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
||||
if (totpCheckbox) {
|
||||
totpCheckbox.checked = false;
|
||||
localStorage.setItem("userTOTPEnabled", "false");
|
||||
}
|
||||
// Call endpoint to remove the TOTP secret from the user's record
|
||||
fetch("totp_disable.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (!result.success) {
|
||||
showToast("Error disabling TOTP setting: " + result.error);
|
||||
}
|
||||
})
|
||||
.catch(() => { showToast("Error disabling TOTP setting."); });
|
||||
}
|
||||
}
|
||||
|
||||
export function openAdminPanel() {
|
||||
fetch("getConfig.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const modalContentStyles = `
|
||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
max-height: 90vh;
|
||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||
`;
|
||||
let adminModal = document.getElementById("adminPanelModal");
|
||||
|
||||
if (!adminModal) {
|
||||
adminModal = document.createElement("div");
|
||||
adminModal.id = "adminPanelModal";
|
||||
adminModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
`;
|
||||
// Added a version number next to "Admin Panel"
|
||||
adminModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeAdminPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>
|
||||
<h3>${adminTitle}</h3>
|
||||
</h3>
|
||||
<form id="adminPanelForm">
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>User Management</legend>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button type="button" id="adminOpenAddUser" class="btn btn-success">Add User</button>
|
||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">Remove User</button>
|
||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">User Permissions</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>OIDC Configuration</legend>
|
||||
<div class="form-group">
|
||||
<label for="oidcProviderUrl">OIDC Provider URL:</label>
|
||||
<input type="text" id="oidcProviderUrl" class="form-control" value="${window.currentOIDCConfig.providerUrl}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcClientId">OIDC Client ID:</label>
|
||||
<input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcClientSecret">OIDC Client Secret:</label>
|
||||
<input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oidcRedirectUri">OIDC Redirect URI:</label>
|
||||
<input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Global TOTP Settings</legend>
|
||||
<div class="form-group">
|
||||
<label for="globalOtpauthUrl">Global OTPAuth URL:</label>
|
||||
<input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-bottom: 15px;">
|
||||
<legend>Login Options</legend>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableFormLogin" />
|
||||
<label for="disableFormLogin">Disable Login Form</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableBasicAuth" />
|
||||
<label for="disableBasicAuth">Disable Basic HTTP Auth</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="disableOIDCLogin" />
|
||||
<label for="disableOIDCLogin">Disable OIDC Login</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="saveAdminSettings" class="btn btn-primary">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(adminModal);
|
||||
|
||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||
adminModal.addEventListener("click", (e) => {
|
||||
if (e.target === adminModal) closeAdminPanel();
|
||||
});
|
||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("adminOpenRemoveUser").addEventListener("click", () => {
|
||||
if (typeof window.loadUserList === "function") {
|
||||
window.loadUserList();
|
||||
}
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
// New event binding for the User Permissions button:
|
||||
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
||||
openUserPermissionsModal();
|
||||
});
|
||||
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
||||
if (totalDisabled === 3) {
|
||||
showToast("At least one login method must remain enabled.");
|
||||
disableOIDCLoginCheckbox.checked = false;
|
||||
localStorage.setItem("disableOIDCLogin", "false");
|
||||
if (typeof window.updateLoginOptionsUI === "function") {
|
||||
window.updateLoginOptionsUI({
|
||||
disableFormLogin: disableFormLoginCheckbox.checked,
|
||||
disableBasicAuth: disableBasicAuthCheckbox.checked,
|
||||
disableOIDCLogin: disableOIDCLoginCheckbox.checked
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const newOIDCConfig = {
|
||||
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||
clientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||
redirectUri: document.getElementById("oidcRedirectUri").value.trim()
|
||||
};
|
||||
const disableFormLogin = disableFormLoginCheckbox.checked;
|
||||
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
||||
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
||||
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
||||
sendRequest("updateConfig.php", "POST", {
|
||||
oidc: newOIDCConfig,
|
||||
disableFormLogin,
|
||||
disableBasicAuth,
|
||||
disableOIDCLogin,
|
||||
globalOtpauthUrl
|
||||
}, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast("Settings updated successfully.");
|
||||
localStorage.setItem("disableFormLogin", disableFormLogin);
|
||||
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
||||
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
||||
if (typeof window.updateLoginOptionsUI === "function") {
|
||||
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
||||
}
|
||||
closeAdminPanel();
|
||||
} else {
|
||||
showToast("Error updating settings: " + (response.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
});
|
||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||
function enforceLoginOptionConstraint(changedCheckbox) {
|
||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
||||
if (changedCheckbox.checked && totalDisabled === 3) {
|
||||
showToast("At least one login method must remain enabled.");
|
||||
changedCheckbox.checked = false;
|
||||
}
|
||||
}
|
||||
disableFormLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||
disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||
disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||
|
||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||
} else {
|
||||
adminModal.style.backgroundColor = overlayBackground;
|
||||
const modalContent = adminModal.querySelector(".modal-content");
|
||||
if (modalContent) {
|
||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||
modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc";
|
||||
}
|
||||
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
||||
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/FileRise?issuer=FileRise';
|
||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||
adminModal.style.display = "flex";
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
let adminModal = document.getElementById("adminPanelModal");
|
||||
if (adminModal) {
|
||||
adminModal.style.backgroundColor = "rgba(0,0,0,0.5)";
|
||||
const modalContent = adminModal.querySelector(".modal-content");
|
||||
if (modalContent) {
|
||||
modalContent.style.background = "#fff";
|
||||
modalContent.style.color = "#000";
|
||||
modalContent.style.border = "1px solid #ccc";
|
||||
}
|
||||
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
||||
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/FileRise?issuer=FileRise';
|
||||
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
||||
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
||||
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
||||
adminModal.style.display = "flex";
|
||||
} else {
|
||||
openAdminPanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function closeAdminPanel() {
|
||||
const adminModal = document.getElementById("adminPanelModal");
|
||||
if (adminModal) adminModal.style.display = "none";
|
||||
}
|
||||
|
||||
// --- New: User Permissions Modal ---
|
||||
|
||||
export function openUserPermissionsModal() {
|
||||
let userPermissionsModal = document.getElementById("userPermissionsModal");
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const modalContentStyles = `
|
||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
if (!userPermissionsModal) {
|
||||
userPermissionsModal = document.createElement("div");
|
||||
userPermissionsModal.id = "userPermissionsModal";
|
||||
userPermissionsModal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3500;
|
||||
`;
|
||||
userPermissionsModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>User Permissions</h3>
|
||||
<div id="userPermissionsList" style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">
|
||||
<!-- User rows will be loaded here -->
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
||||
<button type="button" id="cancelUserPermissionsBtn" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">Save Permissions</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(userPermissionsModal);
|
||||
document.getElementById("closeUserPermissionsModal").addEventListener("click", () => {
|
||||
userPermissionsModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => {
|
||||
userPermissionsModal.style.display = "none";
|
||||
});
|
||||
document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => {
|
||||
// Collect permissions data from each user row.
|
||||
const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
|
||||
const permissionsData = [];
|
||||
rows.forEach(row => {
|
||||
const username = row.getAttribute("data-username");
|
||||
const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']");
|
||||
const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']");
|
||||
const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']");
|
||||
permissionsData.push({
|
||||
username,
|
||||
folderOnly: folderOnlyCheckbox.checked,
|
||||
readOnly: readOnlyCheckbox.checked,
|
||||
disableUpload: disableUploadCheckbox.checked
|
||||
});
|
||||
});
|
||||
// Send the permissionsData to the server.
|
||||
sendRequest("updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
showToast("User permissions updated successfully.");
|
||||
userPermissionsModal.style.display = "none";
|
||||
} else {
|
||||
showToast("Error updating permissions: " + (response.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("Error updating permissions.");
|
||||
});
|
||||
});
|
||||
} else {
|
||||
userPermissionsModal.style.display = "flex";
|
||||
}
|
||||
// Load the list of users into the modal.
|
||||
loadUserPermissionsList();
|
||||
}
|
||||
|
||||
function loadUserPermissionsList() {
|
||||
const listContainer = document.getElementById("userPermissionsList");
|
||||
if (!listContainer) return;
|
||||
listContainer.innerHTML = "";
|
||||
|
||||
// First, fetch the current permissions from the server.
|
||||
fetch("getUserPermissions.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(permissionsData => {
|
||||
// Then, fetch the list of users.
|
||||
return fetch("getUsers.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(usersData => {
|
||||
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
||||
if (users.length === 0) {
|
||||
listContainer.innerHTML = "<p>No users found.</p>";
|
||||
return;
|
||||
}
|
||||
users.forEach(user => {
|
||||
// Skip admin users.
|
||||
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
|
||||
|
||||
// Use stored permissions if available; otherwise fall back to localStorage defaults.
|
||||
const defaultPerm = {
|
||||
folderOnly: localStorage.getItem("folderOnly") === "true",
|
||||
readOnly: localStorage.getItem("readOnly") === "true",
|
||||
disableUpload: localStorage.getItem("disableUpload") === "true"
|
||||
};
|
||||
const userPerm = (permissionsData && typeof permissionsData === "object" && permissionsData[user.username]) || defaultPerm;
|
||||
|
||||
// Create a row for the user.
|
||||
const row = document.createElement("div");
|
||||
row.classList.add("user-permission-row");
|
||||
row.setAttribute("data-username", user.username);
|
||||
row.style.padding = "10px 0";
|
||||
row.innerHTML = `
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">${user.username}</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="folderOnly" ${userPerm.folderOnly ? "checked" : ""} />
|
||||
User Folder Only
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
|
||||
Read Only
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
|
||||
Disable Upload
|
||||
</label>
|
||||
</div>
|
||||
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
||||
`;
|
||||
listContainer.appendChild(row);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
listContainer.innerHTML = "<p>Error loading users.</p>";
|
||||
});
|
||||
}
|
||||
283
js/domUtils.js
Normal file
283
js/domUtils.js
Normal file
@@ -0,0 +1,283 @@
|
||||
// domUtils.js
|
||||
|
||||
// Basic DOM Helpers
|
||||
export function toggleVisibility(elementId, shouldShow) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.style.display = shouldShow ? "block" : "none";
|
||||
} else {
|
||||
console.error(`Element with id "${elementId}" not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeHTML(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export function toggleAllCheckboxes(masterCheckbox) {
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox");
|
||||
checkboxes.forEach(chk => {
|
||||
chk.checked = masterCheckbox.checked;
|
||||
});
|
||||
updateFileActionButtons(); // update buttons based on current selection
|
||||
}
|
||||
|
||||
export function updateFileActionButtons() {
|
||||
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
|
||||
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
||||
const copyBtn = document.getElementById("copySelectedBtn");
|
||||
const moveBtn = document.getElementById("moveSelectedBtn");
|
||||
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
||||
const zipBtn = document.getElementById("downloadZipBtn");
|
||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||
|
||||
if (fileCheckboxes.length === 0) {
|
||||
if (copyBtn) copyBtn.style.display = "none";
|
||||
if (moveBtn) moveBtn.style.display = "none";
|
||||
if (deleteBtn) deleteBtn.style.display = "none";
|
||||
if (zipBtn) zipBtn.style.display = "none";
|
||||
if (extractZipBtn) extractZipBtn.style.display = "none";
|
||||
} else {
|
||||
if (copyBtn) copyBtn.style.display = "inline-block";
|
||||
if (moveBtn) moveBtn.style.display = "inline-block";
|
||||
if (deleteBtn) deleteBtn.style.display = "inline-block";
|
||||
if (zipBtn) zipBtn.style.display = "inline-block";
|
||||
if (extractZipBtn) extractZipBtn.style.display = "inline-block";
|
||||
|
||||
const anySelected = selectedCheckboxes.length > 0;
|
||||
if (copyBtn) copyBtn.disabled = !anySelected;
|
||||
if (moveBtn) moveBtn.disabled = !anySelected;
|
||||
if (deleteBtn) deleteBtn.disabled = !anySelected;
|
||||
if (zipBtn) zipBtn.disabled = !anySelected;
|
||||
|
||||
if (extractZipBtn) {
|
||||
// Enable only if at least one selected file ends with .zip (case-insensitive).
|
||||
const anyZipSelected = Array.from(selectedCheckboxes).some(chk =>
|
||||
chk.value.toLowerCase().endsWith(".zip")
|
||||
);
|
||||
extractZipBtn.disabled = !anyZipSelected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function showToast(message, duration = 3000) {
|
||||
const toast = document.getElementById("customToast");
|
||||
if (!toast) {
|
||||
console.error("Toast element not found");
|
||||
return;
|
||||
}
|
||||
toast.textContent = message;
|
||||
toast.style.display = "block";
|
||||
// Force reflow for transition effect.
|
||||
void toast.offsetWidth;
|
||||
toast.classList.add("show");
|
||||
setTimeout(() => {
|
||||
toast.classList.remove("show");
|
||||
setTimeout(() => {
|
||||
toast.style.display = "none";
|
||||
}, 500);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// --- DOM Building Functions for File Table ---
|
||||
|
||||
export function buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm }) {
|
||||
const safeSearchTerm = escapeHTML(searchTerm);
|
||||
return `
|
||||
<div class="row align-items-center mb-3">
|
||||
<div class="col-12 col-md-8 mb-2 mb-md-0">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" id="searchIcon">
|
||||
<i class="material-icons">search</i>
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Search files or tag..." value="${safeSearchTerm}" aria-describedby="searchIcon">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 text-left">
|
||||
<div class="d-flex justify-content-center justify-content-md-start align-items-center">
|
||||
<button class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""} onclick="changePage(${currentPage - 1})">Prev</button>
|
||||
<span class="page-indicator">Page ${currentPage} of ${totalPages || 1}</span>
|
||||
<button class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""} onclick="changePage(${currentPage + 1})">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildFileTableHeader(sortOrder) {
|
||||
return `
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="checkbox-col"><input type="checkbox" id="selectAll" onclick="toggleAllCheckboxes(this)"></th>
|
||||
<th data-column="name" class="sortable-col">File Name ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
<th data-column="modified" class="hide-small sortable-col">Date Modified ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
<th data-column="uploaded" class="hide-small hide-medium sortable-col">Upload Date ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
<th data-column="size" class="hide-small sortable-col">File Size ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
<th data-column="uploader" class="hide-small hide-medium sortable-col">Uploader ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildFileTableRow(file, folderPath) {
|
||||
const safeFileName = escapeHTML(file.name);
|
||||
const safeModified = escapeHTML(file.modified);
|
||||
const safeUploaded = escapeHTML(file.uploaded);
|
||||
const safeSize = escapeHTML(file.size);
|
||||
const safeUploader = escapeHTML(file.uploader || "Unknown");
|
||||
|
||||
let previewButton = "";
|
||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|mp3|wav|m4a|ogg|flac|aac|wma|opus|mkv|ogv)$/i.test(file.name)) {
|
||||
let previewIcon = "";
|
||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) {
|
||||
previewIcon = `<i class="material-icons">image</i>`;
|
||||
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(file.name)) {
|
||||
previewIcon = `<i class="material-icons">videocam</i>`;
|
||||
} else if (/\.pdf$/i.test(file.name)) {
|
||||
previewIcon = `<i class="material-icons">picture_as_pdf</i>`;
|
||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||
previewIcon = `<i class="material-icons">audiotrack</i>`;
|
||||
}
|
||||
previewButton = `<button class="btn btn-sm btn-info preview-btn" onclick="event.stopPropagation(); previewFile('${folderPath + encodeURIComponent(file.name)}', '${safeFileName}')">
|
||||
${previewIcon}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<tr onclick="toggleRowSelection(event, '${safeFileName}')" class="clickable-row">
|
||||
<td>
|
||||
<input type="checkbox" class="file-checkbox" value="${safeFileName}" onclick="event.stopPropagation(); updateRowHighlight(this);">
|
||||
</td>
|
||||
<td class="file-name-cell">${safeFileName}</td>
|
||||
<td class="hide-small nowrap">${safeModified}</td>
|
||||
<td class="hide-small hide-medium nowrap">${safeUploaded}</td>
|
||||
<td class="hide-small nowrap">${safeSize}</td>
|
||||
<td class="hide-small hide-medium nowrap">${safeUploader}</td>
|
||||
<td>
|
||||
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
|
||||
<a class="btn btn-sm btn-success download-btn"
|
||||
href="download.php?folder=${encodeURIComponent(file.folder || 'root')}&file=${encodeURIComponent(file.name)}"
|
||||
title="Download">
|
||||
<i class="material-icons">file_download</i>
|
||||
</a>
|
||||
${file.editable ? `
|
||||
<button class="btn btn-sm edit-btn"
|
||||
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
title="Edit">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>
|
||||
` : ""}
|
||||
${previewButton}
|
||||
<button class="btn btn-sm btn-warning rename-btn"
|
||||
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||
title="Rename">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildBottomControls(itemsPerPageSetting) {
|
||||
return `
|
||||
<div class="d-flex align-items-center mt-3 bottom-controls">
|
||||
<label class="label-inline mr-2 mb-0">Show</label>
|
||||
<select class="form-control bottom-select" onchange="changeItemsPerPage(this.value)">
|
||||
${[10, 20, 50, 100].map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`).join("")}
|
||||
</select>
|
||||
<span class="items-per-page-text ml-2 mb-0">items per page</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// --- Global Helper Functions ---
|
||||
|
||||
export function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
export function updateRowHighlight(checkbox) {
|
||||
const row = checkbox.closest('tr');
|
||||
if (!row) return;
|
||||
if (checkbox.checked) {
|
||||
row.classList.add('row-selected');
|
||||
} else {
|
||||
row.classList.remove('row-selected');
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleRowSelection(event, fileName) {
|
||||
const targetTag = event.target.tagName.toLowerCase();
|
||||
if (targetTag === 'a' || targetTag === 'button' || targetTag === 'input') {
|
||||
return;
|
||||
}
|
||||
const row = event.currentTarget;
|
||||
const checkbox = row.querySelector('.file-checkbox');
|
||||
if (!checkbox) return;
|
||||
checkbox.checked = !checkbox.checked;
|
||||
updateRowHighlight(checkbox);
|
||||
updateFileActionButtons();
|
||||
}
|
||||
|
||||
export function attachEnterKeyListener(modalId, buttonId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
// Make the modal focusable
|
||||
modal.setAttribute("tabindex", "-1");
|
||||
modal.focus();
|
||||
modal.addEventListener("keydown", function(e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById(buttonId);
|
||||
if (btn) {
|
||||
btn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function showCustomConfirmModal(message) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.getElementById("customConfirmModal");
|
||||
const messageElem = document.getElementById("confirmMessage");
|
||||
const yesBtn = document.getElementById("confirmYesBtn");
|
||||
const noBtn = document.getElementById("confirmNoBtn");
|
||||
|
||||
messageElem.textContent = message;
|
||||
modal.style.display = "block";
|
||||
|
||||
// Cleanup function to hide the modal and remove event listeners.
|
||||
function cleanup() {
|
||||
modal.style.display = "none";
|
||||
yesBtn.removeEventListener("click", onYes);
|
||||
noBtn.removeEventListener("click", onNo);
|
||||
}
|
||||
|
||||
function onYes() {
|
||||
cleanup();
|
||||
resolve(true);
|
||||
}
|
||||
function onNo() {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
|
||||
yesBtn.addEventListener("click", onYes);
|
||||
noBtn.addEventListener("click", onNo);
|
||||
});
|
||||
}
|
||||
599
js/dragAndDrop.js
Normal file
599
js/dragAndDrop.js
Normal file
@@ -0,0 +1,599 @@
|
||||
// dragAndDrop.js
|
||||
// This file handles drag-and-drop functionality for cards in the sidebar, header and top drop zones.
|
||||
// It also manages the visibility of the sidebar and header drop zones based on the current state of the application.
|
||||
// It includes functions to save and load the order of cards in the sidebar and header from localStorage.
|
||||
// It also includes functions to handle the drag-and-drop events, including mouse movements and drop zones.
|
||||
// It uses CSS classes to manage the appearance of the sidebar and header drop zones during drag-and-drop operations.
|
||||
|
||||
// Moves cards into the sidebar based on the saved order in localStorage.
|
||||
export function loadSidebarOrder() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const orderStr = localStorage.getItem('sidebarOrder');
|
||||
if (orderStr) {
|
||||
const order = JSON.parse(orderStr);
|
||||
if (order.length > 0) {
|
||||
// Ensure main wrapper is visible.
|
||||
const mainWrapper = document.querySelector('.main-wrapper');
|
||||
if (mainWrapper) {
|
||||
mainWrapper.style.display = 'flex';
|
||||
}
|
||||
// For each saved ID, move the card into the sidebar.
|
||||
order.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card && card.parentNode.id !== 'sidebarDropArea') {
|
||||
sidebar.appendChild(card);
|
||||
// Animate vertical slide for sidebar card
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
updateSidebarVisibility();
|
||||
}
|
||||
|
||||
// NEW: Load header order from localStorage.
|
||||
export function loadHeaderOrder() {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (!headerDropArea) return;
|
||||
const orderStr = localStorage.getItem('headerOrder');
|
||||
if (orderStr) {
|
||||
const order = JSON.parse(orderStr);
|
||||
if (order.length > 0) {
|
||||
order.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
// Only load if card is not already in header drop zone.
|
||||
if (card && card.parentNode.id !== 'headerDropArea') {
|
||||
insertCardInHeader(card, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal helper: update sidebar visibility based on its content.
|
||||
function updateSidebarVisibility() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
if (cards.length > 0) {
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = 'block';
|
||||
} else {
|
||||
sidebar.classList.remove('active');
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
// Save the current order in localStorage.
|
||||
saveSidebarOrder();
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Save header order to localStorage.
|
||||
function saveHeaderOrder() {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (headerDropArea) {
|
||||
const icons = Array.from(headerDropArea.children);
|
||||
// Each header icon stores its associated card in the property cardElement.
|
||||
const order = icons.map(icon => icon.cardElement.id);
|
||||
localStorage.setItem('headerOrder', JSON.stringify(order));
|
||||
}
|
||||
}
|
||||
|
||||
// Internal helper: update top zone layout (center a card if one column is empty).
|
||||
function updateTopZoneLayout() {
|
||||
const leftCol = document.getElementById('leftCol');
|
||||
const rightCol = document.getElementById('rightCol');
|
||||
|
||||
const leftIsEmpty = !leftCol.querySelector('#uploadCard');
|
||||
const rightIsEmpty = !rightCol.querySelector('#folderManagementCard');
|
||||
|
||||
if (leftIsEmpty && !rightIsEmpty) {
|
||||
leftCol.style.display = 'none';
|
||||
rightCol.style.margin = '0 auto';
|
||||
} else if (rightIsEmpty && !leftIsEmpty) {
|
||||
rightCol.style.display = 'none';
|
||||
leftCol.style.margin = '0 auto';
|
||||
} else {
|
||||
leftCol.style.display = '';
|
||||
rightCol.style.display = '';
|
||||
leftCol.style.margin = '';
|
||||
rightCol.style.margin = '';
|
||||
}
|
||||
}
|
||||
|
||||
// When a card is being dragged, if the top drop zone is empty, set its min-height.
|
||||
function addTopZoneHighlight() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (topZone) {
|
||||
topZone.classList.add('highlight');
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
topZone.style.minHeight = '375px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When the drag ends, remove the extra min-height.
|
||||
function removeTopZoneHighlight() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (topZone) {
|
||||
topZone.classList.remove('highlight');
|
||||
topZone.style.minHeight = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical slide/fade animation helper.
|
||||
function animateVerticalSlide(card) {
|
||||
card.style.transform = 'translateY(30px)';
|
||||
card.style.opacity = '0';
|
||||
// Force reflow.
|
||||
card.offsetWidth;
|
||||
requestAnimationFrame(() => {
|
||||
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
||||
card.style.transform = 'translateY(0)';
|
||||
card.style.opacity = '1';
|
||||
});
|
||||
setTimeout(() => {
|
||||
card.style.transition = '';
|
||||
card.style.transform = '';
|
||||
card.style.opacity = '';
|
||||
}, 310);
|
||||
}
|
||||
|
||||
// Internal helper: insert card into sidebar at a proper position based on event.clientY.
|
||||
function insertCardInSidebar(card, event) {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||
let inserted = false;
|
||||
for (const currentCard of existingCards) {
|
||||
const rect = currentCard.getBoundingClientRect();
|
||||
const midY = rect.top + rect.height / 2;
|
||||
if (event.clientY < midY) {
|
||||
sidebar.insertBefore(card, currentCard);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
sidebar.appendChild(card);
|
||||
}
|
||||
// Ensure card fills the sidebar.
|
||||
card.style.width = '100%';
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
|
||||
// Internal helper: save the current sidebar card order to localStorage.
|
||||
function saveSidebarOrder() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
const order = Array.from(cards).map(card => card.id);
|
||||
localStorage.setItem('sidebarOrder', JSON.stringify(order));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: move cards from sidebar back to the top drop area when on small screens.
|
||||
function moveSidebarCardsToTop() {
|
||||
if (window.innerWidth < 1205) {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||
cards.forEach(card => {
|
||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||
if (orig) {
|
||||
orig.appendChild(card);
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
});
|
||||
updateSidebarVisibility();
|
||||
updateTopZoneLayout();
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for window resize to automatically move sidebar cards back to top on small screens.
|
||||
window.addEventListener('resize', function () {
|
||||
if (window.innerWidth < 1205) {
|
||||
moveSidebarCardsToTop();
|
||||
}
|
||||
});
|
||||
|
||||
// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
|
||||
function ensureTopZonePlaceholder() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (!topZone) return;
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
let placeholder = topZone.querySelector('.placeholder');
|
||||
if (!placeholder) {
|
||||
placeholder = document.createElement('div');
|
||||
placeholder.className = 'placeholder';
|
||||
placeholder.style.visibility = 'hidden';
|
||||
placeholder.style.display = 'block';
|
||||
placeholder.style.width = '100%';
|
||||
placeholder.style.height = '375px';
|
||||
topZone.appendChild(placeholder);
|
||||
}
|
||||
} else {
|
||||
const placeholder = topZone.querySelector('.placeholder');
|
||||
if (placeholder) placeholder.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// --- NEW HELPER FUNCTIONS FOR HEADER DROP ZONE ---
|
||||
|
||||
// Show header drop zone and add a "drag-active" class so that the pseudo-element appears.
|
||||
function showHeaderDropZone() {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (headerDropArea) {
|
||||
headerDropArea.style.display = 'inline-flex';
|
||||
headerDropArea.classList.add('drag-active');
|
||||
}
|
||||
}
|
||||
|
||||
// Hide header drop zone by removing the "drag-active" class.
|
||||
// If a header icon is present (i.e. a card was dropped), the drop zone remains visible without the dashed border.
|
||||
function hideHeaderDropZone() {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (headerDropArea) {
|
||||
headerDropArea.classList.remove('drag-active');
|
||||
if (headerDropArea.children.length === 0) {
|
||||
headerDropArea.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === NEW FUNCTION: Insert card into header drop zone as a material icon ===
|
||||
function insertCardInHeader(card, event) {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (!headerDropArea) return;
|
||||
|
||||
// For folder management and upload cards, preserve the original by moving it to a hidden container.
|
||||
if (card.id === 'folderManagementCard' || card.id === 'uploadCard') {
|
||||
let hiddenContainer = document.getElementById('hiddenCardsContainer');
|
||||
if (!hiddenContainer) {
|
||||
hiddenContainer = document.createElement('div');
|
||||
hiddenContainer.id = 'hiddenCardsContainer';
|
||||
hiddenContainer.style.display = 'none';
|
||||
document.body.appendChild(hiddenContainer);
|
||||
}
|
||||
// Move the original card to the hidden container if it's not already there.
|
||||
if (card.parentNode.id !== 'hiddenCardsContainer') {
|
||||
hiddenContainer.appendChild(card);
|
||||
}
|
||||
} else {
|
||||
// For other cards, simply remove from current container.
|
||||
if (card.parentNode) {
|
||||
card.parentNode.removeChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the header icon button.
|
||||
const iconButton = document.createElement('button');
|
||||
iconButton.className = 'header-card-icon';
|
||||
// Remove default button styling.
|
||||
iconButton.style.border = 'none';
|
||||
iconButton.style.background = 'none';
|
||||
iconButton.style.outline = 'none';
|
||||
iconButton.style.cursor = 'pointer';
|
||||
|
||||
// Choose an icon based on the card type with 24px size.
|
||||
if (card.id === 'uploadCard') {
|
||||
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">cloud_upload</i>';
|
||||
} else if (card.id === 'folderManagementCard') {
|
||||
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">folder</i>';
|
||||
} else {
|
||||
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">insert_drive_file</i>';
|
||||
}
|
||||
|
||||
// Save a reference to the card in the icon button.
|
||||
iconButton.cardElement = card;
|
||||
// Associate this icon with the card for future removal.
|
||||
card.headerIconButton = iconButton;
|
||||
|
||||
let modal = null;
|
||||
let isLocked = false;
|
||||
let hoverActive = false;
|
||||
|
||||
// showModal: When triggered, ensure the card is attached to the modal.
|
||||
function showModal() {
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.className = 'header-card-modal';
|
||||
modal.style.position = 'fixed';
|
||||
modal.style.top = '80px';
|
||||
modal.style.right = '80px';
|
||||
modal.style.zIndex = '11000';
|
||||
// Render the modal but initially keep it hidden.
|
||||
modal.style.display = 'block';
|
||||
modal.style.visibility = 'hidden';
|
||||
modal.style.opacity = '0';
|
||||
modal.style.background = 'none';
|
||||
modal.style.border = 'none';
|
||||
modal.style.padding = '0';
|
||||
modal.style.boxShadow = 'none';
|
||||
document.body.appendChild(modal);
|
||||
// Attach modal hover events.
|
||||
modal.addEventListener('mouseover', handleMouseOver);
|
||||
modal.addEventListener('mouseout', handleMouseOut);
|
||||
iconButton.modalInstance = modal;
|
||||
}
|
||||
// If the card isn't already in the modal, remove it from the hidden container and attach it.
|
||||
if (!modal.contains(card)) {
|
||||
const hiddenContainer = document.getElementById('hiddenCardsContainer');
|
||||
if (hiddenContainer && hiddenContainer.contains(card)) {
|
||||
hiddenContainer.removeChild(card);
|
||||
}
|
||||
modal.appendChild(card);
|
||||
}
|
||||
// Reveal the modal.
|
||||
modal.style.visibility = 'visible';
|
||||
modal.style.opacity = '1';
|
||||
}
|
||||
|
||||
// hideModal: Hide the modal and return the card to the hidden container.
|
||||
function hideModal() {
|
||||
if (modal && !isLocked && !hoverActive) {
|
||||
modal.style.visibility = 'hidden';
|
||||
modal.style.opacity = '0';
|
||||
// Return the card to the hidden container.
|
||||
const hiddenContainer = document.getElementById('hiddenCardsContainer');
|
||||
if (hiddenContainer && modal.contains(card)) {
|
||||
hiddenContainer.appendChild(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseOver() {
|
||||
hoverActive = true;
|
||||
showModal();
|
||||
}
|
||||
|
||||
function handleMouseOut() {
|
||||
hoverActive = false;
|
||||
setTimeout(() => {
|
||||
if (!hoverActive && !isLocked) {
|
||||
hideModal();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Attach hover events to the icon.
|
||||
iconButton.addEventListener('mouseover', handleMouseOver);
|
||||
iconButton.addEventListener('mouseout', handleMouseOut);
|
||||
|
||||
// Toggle the locked state on click so the modal stays open.
|
||||
iconButton.addEventListener('click', (e) => {
|
||||
isLocked = !isLocked;
|
||||
if (isLocked) {
|
||||
showModal();
|
||||
} else {
|
||||
hideModal();
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Append the header icon button into the header drop zone.
|
||||
headerDropArea.appendChild(iconButton);
|
||||
// Save the updated header order.
|
||||
saveHeaderOrder();
|
||||
}
|
||||
|
||||
// === Main Drag and Drop Initialization ===
|
||||
export function initDragAndDrop() {
|
||||
function run() {
|
||||
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
draggableCards.forEach(card => {
|
||||
if (!card.dataset.originalContainerId) {
|
||||
card.dataset.originalContainerId = card.parentNode.id;
|
||||
}
|
||||
const header = card.querySelector('.card-header');
|
||||
if (header) {
|
||||
header.classList.add('drag-header');
|
||||
}
|
||||
|
||||
let isDragging = false;
|
||||
let dragTimer = null;
|
||||
let offsetX = 0, offsetY = 0;
|
||||
let initialLeft, initialTop;
|
||||
|
||||
if (header) {
|
||||
header.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
const card = this.closest('.card');
|
||||
// Capture the card's initial bounding rectangle.
|
||||
const initialRect = card.getBoundingClientRect();
|
||||
const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100;
|
||||
const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100;
|
||||
card.style.transformOrigin = `${originX}% ${originY}%`;
|
||||
|
||||
// Store the initial rect so we use it later.
|
||||
dragTimer = setTimeout(() => {
|
||||
isDragging = true;
|
||||
card.classList.add('dragging');
|
||||
card.style.pointerEvents = 'none';
|
||||
addTopZoneHighlight();
|
||||
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = 'block';
|
||||
sidebar.classList.add('highlight');
|
||||
sidebar.style.height = '800px';
|
||||
}
|
||||
|
||||
// Show header drop zone while dragging.
|
||||
showHeaderDropZone();
|
||||
|
||||
// Use the stored initialRect.
|
||||
initialLeft = initialRect.left + window.pageXOffset;
|
||||
initialTop = initialRect.top + window.pageYOffset;
|
||||
offsetX = e.pageX - initialLeft;
|
||||
offsetY = e.pageY - initialTop;
|
||||
|
||||
// Remove any associated header icon if present.
|
||||
if (card.headerIconButton) {
|
||||
if (card.headerIconButton.parentNode) {
|
||||
card.headerIconButton.parentNode.removeChild(card.headerIconButton);
|
||||
}
|
||||
if (card.headerIconButton.modalInstance && card.headerIconButton.modalInstance.parentNode) {
|
||||
card.headerIconButton.modalInstance.parentNode.removeChild(card.headerIconButton.modalInstance);
|
||||
}
|
||||
card.headerIconButton = null;
|
||||
saveHeaderOrder();
|
||||
}
|
||||
|
||||
// Append card to body and fix its dimensions.
|
||||
document.body.appendChild(card);
|
||||
card.style.position = 'absolute';
|
||||
card.style.left = initialLeft + 'px';
|
||||
card.style.top = initialTop + 'px';
|
||||
card.style.width = initialRect.width + 'px';
|
||||
card.style.height = initialRect.height + 'px';
|
||||
card.style.minWidth = initialRect.width + 'px';
|
||||
card.style.flexShrink = '0';
|
||||
card.style.zIndex = '10000';
|
||||
}, 500);
|
||||
});
|
||||
header.addEventListener('mouseup', function () {
|
||||
clearTimeout(dragTimer);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
if (isDragging) {
|
||||
card.style.left = (e.pageX - offsetX) + 'px';
|
||||
card.style.top = (e.pageY - offsetY) + 'px';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', function (e) {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
card.style.pointerEvents = '';
|
||||
card.classList.remove('dragging');
|
||||
removeTopZoneHighlight();
|
||||
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('highlight');
|
||||
sidebar.style.height = '';
|
||||
}
|
||||
|
||||
// Remove any existing header icon if present.
|
||||
if (card.headerIconButton) {
|
||||
if (card.headerIconButton.parentNode) {
|
||||
card.headerIconButton.parentNode.removeChild(card.headerIconButton);
|
||||
}
|
||||
if (card.headerIconButton.modalInstance && card.headerIconButton.modalInstance.parentNode) {
|
||||
card.headerIconButton.modalInstance.parentNode.removeChild(card.headerIconButton.modalInstance);
|
||||
}
|
||||
card.headerIconButton = null;
|
||||
saveHeaderOrder();
|
||||
}
|
||||
|
||||
let droppedInSidebar = false;
|
||||
let droppedInTop = false;
|
||||
let droppedInHeader = false;
|
||||
|
||||
// Check if dropped in sidebar drop zone.
|
||||
const sidebarElem = document.getElementById('sidebarDropArea');
|
||||
if (sidebarElem) {
|
||||
const rect = sidebarElem.getBoundingClientRect();
|
||||
const dropZoneBottom = rect.top + 800; // Virtual drop zone height.
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= dropZoneBottom
|
||||
) {
|
||||
insertCardInSidebar(card, e);
|
||||
droppedInSidebar = true;
|
||||
}
|
||||
}
|
||||
// Check the top drop zone.
|
||||
const topRow = document.getElementById('uploadFolderRow');
|
||||
if (!droppedInSidebar && topRow) {
|
||||
const rect = topRow.getBoundingClientRect();
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= rect.bottom
|
||||
) {
|
||||
let container;
|
||||
if (card.id === 'uploadCard') {
|
||||
container = document.getElementById('leftCol');
|
||||
} else if (card.id === 'folderManagementCard') {
|
||||
container = document.getElementById('rightCol');
|
||||
}
|
||||
if (container) {
|
||||
ensureTopZonePlaceholder();
|
||||
updateTopZoneLayout();
|
||||
container.appendChild(card);
|
||||
droppedInTop = true;
|
||||
// Set a fixed width during animation.
|
||||
card.style.width = "363px";
|
||||
animateVerticalSlide(card);
|
||||
setTimeout(() => {
|
||||
card.style.removeProperty('width');
|
||||
}, 210);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check the header drop zone.
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (!droppedInSidebar && !droppedInTop && headerDropArea) {
|
||||
const rect = headerDropArea.getBoundingClientRect();
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= rect.bottom
|
||||
) {
|
||||
insertCardInHeader(card, e);
|
||||
droppedInHeader = true;
|
||||
}
|
||||
}
|
||||
// If card was not dropped in any zone, return it to its original container.
|
||||
if (!droppedInSidebar && !droppedInTop && !droppedInHeader) {
|
||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||
if (orig) {
|
||||
orig.appendChild(card);
|
||||
card.style.removeProperty('width');
|
||||
}
|
||||
}
|
||||
|
||||
// Clear inline drag-related styles.
|
||||
[
|
||||
'position',
|
||||
'left',
|
||||
'top',
|
||||
'z-index',
|
||||
'height',
|
||||
'min-width',
|
||||
'flex-shrink',
|
||||
'transition',
|
||||
'transform',
|
||||
'opacity'
|
||||
].forEach(prop => card.style.removeProperty(prop));
|
||||
|
||||
// For sidebar drops, force width to 100%.
|
||||
if (droppedInSidebar) {
|
||||
card.style.width = '100%';
|
||||
}
|
||||
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
|
||||
// Hide header drop zone if no icon is present.
|
||||
hideHeaderDropZone();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
1652
js/fileManager.js
Normal file
1652
js/fileManager.js
Normal file
File diff suppressed because it is too large
Load Diff
466
js/fileTags.js
Normal file
466
js/fileTags.js
Normal file
@@ -0,0 +1,466 @@
|
||||
// fileTags.js
|
||||
// This module provides functions for opening the tag modal,
|
||||
// adding tags to files (with a global tag store for reuse),
|
||||
// updating the file row display with tag badges,
|
||||
// filtering the file list by tag, and persisting tag data.
|
||||
import { escapeHTML } from './domUtils.js';
|
||||
|
||||
export function openTagModal(file) {
|
||||
// Create the modal element.
|
||||
let modal = document.createElement('div');
|
||||
modal.id = 'tagModal';
|
||||
modal.className = 'modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="width: 400px; max-width:90vw;">
|
||||
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0;">Tag File: ${file.name}</h3>
|
||||
<span id="closeTagModal" style="cursor:pointer; font-size:24px;">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="margin-top:10px;">
|
||||
<label for="tagNameInput">Tag Name:</label>
|
||||
<input type="text" id="tagNameInput" placeholder="Enter tag name" style="width:100%; padding:5px;"/>
|
||||
<br><br>
|
||||
<label for="tagColorInput">Tag Color:</label>
|
||||
<input type="color" id="tagColorInput" value="#ff0000" style="width:100%; padding:5px;"/>
|
||||
<br><br>
|
||||
<div id="customTagDropdown" style="max-height:150px; overflow-y:auto; border:1px solid #ccc; margin-top:5px; padding:5px;">
|
||||
<!-- Custom tag options will be populated here -->
|
||||
</div>
|
||||
<br>
|
||||
<div style="text-align:right;">
|
||||
<button id="saveTagBtn" class="btn btn-primary">Save Tag</button>
|
||||
</div>
|
||||
<div id="currentTags" style="margin-top:10px; font-size:0.9em;">
|
||||
<!-- Existing tags will be listed here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = 'block';
|
||||
|
||||
updateCustomTagDropdown();
|
||||
|
||||
document.getElementById('closeTagModal').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
updateTagModalDisplay(file);
|
||||
|
||||
document.getElementById('tagNameInput').addEventListener('input', (e) => {
|
||||
updateCustomTagDropdown(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('saveTagBtn').addEventListener('click', () => {
|
||||
const tagName = document.getElementById('tagNameInput').value.trim();
|
||||
const tagColor = document.getElementById('tagColorInput').value;
|
||||
if (!tagName) {
|
||||
alert('Please enter a tag name.');
|
||||
return;
|
||||
}
|
||||
addTagToFile(file, { name: tagName, color: tagColor });
|
||||
updateTagModalDisplay(file);
|
||||
updateFileRowTagDisplay(file);
|
||||
saveFileTags(file);
|
||||
document.getElementById('tagNameInput').value = '';
|
||||
updateCustomTagDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a modal to tag multiple files.
|
||||
* @param {Array} files - Array of file objects to tag.
|
||||
*/
|
||||
export function openMultiTagModal(files) {
|
||||
let modal = document.createElement('div');
|
||||
modal.id = 'multiTagModal';
|
||||
modal.className = 'modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="width: 400px; max-width:90vw;">
|
||||
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0;">Tag Selected Files (${files.length})</h3>
|
||||
<span id="closeMultiTagModal" style="cursor:pointer; font-size:24px;">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="margin-top:10px;">
|
||||
<label for="multiTagNameInput">Tag Name:</label>
|
||||
<input type="text" id="multiTagNameInput" placeholder="Enter tag name" style="width:100%; padding:5px;"/>
|
||||
<br><br>
|
||||
<label for="multiTagColorInput">Tag Color:</label>
|
||||
<input type="color" id="multiTagColorInput" value="#ff0000" style="width:100%; padding:5px;"/>
|
||||
<br><br>
|
||||
<div id="multiCustomTagDropdown" style="max-height:150px; overflow-y:auto; border:1px solid #ccc; margin-top:5px; padding:5px;">
|
||||
<!-- Custom tag options will be populated here -->
|
||||
</div>
|
||||
<br>
|
||||
<div style="text-align:right;">
|
||||
<button id="saveMultiTagBtn" class="btn btn-primary">Save Tag to Selected</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = 'block';
|
||||
|
||||
updateMultiCustomTagDropdown();
|
||||
|
||||
document.getElementById('closeMultiTagModal').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
document.getElementById('multiTagNameInput').addEventListener('input', (e) => {
|
||||
updateMultiCustomTagDropdown(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('saveMultiTagBtn').addEventListener('click', () => {
|
||||
const tagName = document.getElementById('multiTagNameInput').value.trim();
|
||||
const tagColor = document.getElementById('multiTagColorInput').value;
|
||||
if (!tagName) {
|
||||
alert('Please enter a tag name.');
|
||||
return;
|
||||
}
|
||||
files.forEach(file => {
|
||||
addTagToFile(file, { name: tagName, color: tagColor });
|
||||
updateFileRowTagDisplay(file);
|
||||
saveFileTags(file);
|
||||
});
|
||||
modal.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the custom dropdown for multi-tag modal.
|
||||
* Similar to updateCustomTagDropdown but includes a remove icon.
|
||||
*/
|
||||
function updateMultiCustomTagDropdown(filterText = "") {
|
||||
const dropdown = document.getElementById("multiCustomTagDropdown");
|
||||
if (!dropdown) return;
|
||||
dropdown.innerHTML = "";
|
||||
let tags = window.globalTags || [];
|
||||
if (filterText) {
|
||||
tags = tags.filter(tag => tag.name.toLowerCase().includes(filterText.toLowerCase()));
|
||||
}
|
||||
if (tags.length > 0) {
|
||||
tags.forEach(tag => {
|
||||
const item = document.createElement("div");
|
||||
item.style.cursor = "pointer";
|
||||
item.style.padding = "5px";
|
||||
item.style.borderBottom = "1px solid #eee";
|
||||
// Display colored square and tag name with remove icon.
|
||||
item.innerHTML = `
|
||||
<span style="display:inline-block; width:16px; height:16px; background-color:${tag.color}; border:1px solid #ccc; margin-right:5px; vertical-align:middle;"></span>
|
||||
${escapeHTML(tag.name)}
|
||||
<span class="global-remove" style="color:red; font-weight:bold; margin-left:5px; cursor:pointer;">×</span>
|
||||
`;
|
||||
item.addEventListener("click", function(e) {
|
||||
if (e.target.classList.contains("global-remove")) return;
|
||||
document.getElementById("multiTagNameInput").value = tag.name;
|
||||
document.getElementById("multiTagColorInput").value = tag.color;
|
||||
});
|
||||
item.querySelector('.global-remove').addEventListener("click", function(e){
|
||||
e.stopPropagation();
|
||||
removeGlobalTag(tag.name);
|
||||
});
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
} else {
|
||||
dropdown.innerHTML = "<div style='padding:5px;'>No tags available</div>";
|
||||
}
|
||||
}
|
||||
|
||||
function updateCustomTagDropdown(filterText = "") {
|
||||
const dropdown = document.getElementById("customTagDropdown");
|
||||
if (!dropdown) return;
|
||||
dropdown.innerHTML = "";
|
||||
let tags = window.globalTags || [];
|
||||
if (filterText) {
|
||||
tags = tags.filter(tag => tag.name.toLowerCase().includes(filterText.toLowerCase()));
|
||||
}
|
||||
if (tags.length > 0) {
|
||||
tags.forEach(tag => {
|
||||
const item = document.createElement("div");
|
||||
item.style.cursor = "pointer";
|
||||
item.style.padding = "5px";
|
||||
item.style.borderBottom = "1px solid #eee";
|
||||
item.innerHTML = `
|
||||
<span style="display:inline-block; width:16px; height:16px; background-color:${tag.color}; border:1px solid #ccc; margin-right:5px; vertical-align:middle;"></span>
|
||||
${escapeHTML(tag.name)}
|
||||
<span class="global-remove" style="color:red; font-weight:bold; margin-left:5px; cursor:pointer;">×</span>
|
||||
`;
|
||||
item.addEventListener("click", function(e){
|
||||
if (e.target.classList.contains('global-remove')) return;
|
||||
document.getElementById("tagNameInput").value = tag.name;
|
||||
document.getElementById("tagColorInput").value = tag.color;
|
||||
});
|
||||
item.querySelector('.global-remove').addEventListener("click", function(e){
|
||||
e.stopPropagation();
|
||||
removeGlobalTag(tag.name);
|
||||
});
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
} else {
|
||||
dropdown.innerHTML = "<div style='padding:5px;'>No tags available</div>";
|
||||
}
|
||||
}
|
||||
|
||||
// Update the modal display to show current tags on the file.
|
||||
function updateTagModalDisplay(file) {
|
||||
const container = document.getElementById('currentTags');
|
||||
if (!container) return;
|
||||
container.innerHTML = '<strong>Current Tags:</strong> ';
|
||||
if (file.tags && file.tags.length > 0) {
|
||||
file.tags.forEach(tag => {
|
||||
const tagElem = document.createElement('span');
|
||||
tagElem.textContent = tag.name;
|
||||
tagElem.style.backgroundColor = tag.color;
|
||||
tagElem.style.color = '#fff';
|
||||
tagElem.style.padding = '2px 6px';
|
||||
tagElem.style.marginRight = '5px';
|
||||
tagElem.style.borderRadius = '3px';
|
||||
tagElem.style.display = 'inline-block';
|
||||
tagElem.style.position = 'relative';
|
||||
|
||||
const removeIcon = document.createElement('span');
|
||||
removeIcon.textContent = ' ✕';
|
||||
removeIcon.style.fontWeight = 'bold';
|
||||
removeIcon.style.marginLeft = '3px';
|
||||
removeIcon.style.cursor = 'pointer';
|
||||
|
||||
removeIcon.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
removeTagFromFile(file, tag.name);
|
||||
});
|
||||
|
||||
tagElem.appendChild(removeIcon);
|
||||
container.appendChild(tagElem);
|
||||
});
|
||||
} else {
|
||||
container.innerHTML += 'None';
|
||||
}
|
||||
}
|
||||
|
||||
function removeTagFromFile(file, tagName) {
|
||||
file.tags = file.tags.filter(t => t.name.toLowerCase() !== tagName.toLowerCase());
|
||||
updateTagModalDisplay(file);
|
||||
updateFileRowTagDisplay(file);
|
||||
saveFileTags(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tag from the global tag store.
|
||||
* This function updates window.globalTags and calls the backend endpoint
|
||||
* to remove the tag from the persistent store.
|
||||
*/
|
||||
function removeGlobalTag(tagName) {
|
||||
window.globalTags = window.globalTags.filter(t => t.name.toLowerCase() !== tagName.toLowerCase());
|
||||
localStorage.setItem('globalTags', JSON.stringify(window.globalTags));
|
||||
updateCustomTagDropdown();
|
||||
updateMultiCustomTagDropdown();
|
||||
saveGlobalTagRemoval(tagName);
|
||||
}
|
||||
|
||||
// NEW: Save global tag removal to the server.
|
||||
function saveGlobalTagRemoval(tagName) {
|
||||
fetch("saveFileTag.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: "root",
|
||||
file: "global",
|
||||
deleteGlobal: true,
|
||||
tagToDelete: tagName,
|
||||
tags: []
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log("Global tag removed:", tagName);
|
||||
if (data.globalTags) {
|
||||
window.globalTags = data.globalTags;
|
||||
localStorage.setItem('globalTags', JSON.stringify(window.globalTags));
|
||||
updateCustomTagDropdown();
|
||||
updateMultiCustomTagDropdown();
|
||||
}
|
||||
} else {
|
||||
console.error("Error removing global tag:", data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error removing global tag:", err);
|
||||
});
|
||||
}
|
||||
|
||||
// Global store for reusable tags.
|
||||
window.globalTags = window.globalTags || [];
|
||||
if (localStorage.getItem('globalTags')) {
|
||||
try {
|
||||
window.globalTags = JSON.parse(localStorage.getItem('globalTags'));
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
// New function to load global tags from the server's persistent JSON.
|
||||
export function loadGlobalTags() {
|
||||
fetch("metadata/createdTags.json", { credentials: "include" })
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
// If the file doesn't exist, assume there are no global tags.
|
||||
return [];
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
window.globalTags = data;
|
||||
localStorage.setItem('globalTags', JSON.stringify(window.globalTags));
|
||||
updateCustomTagDropdown();
|
||||
updateMultiCustomTagDropdown();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error loading global tags:", err);
|
||||
window.globalTags = [];
|
||||
updateCustomTagDropdown();
|
||||
updateMultiCustomTagDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
loadGlobalTags();
|
||||
|
||||
// Add (or update) a tag in the file object.
|
||||
export function addTagToFile(file, tag) {
|
||||
if (!file.tags) {
|
||||
file.tags = [];
|
||||
}
|
||||
const exists = file.tags.find(t => t.name.toLowerCase() === tag.name.toLowerCase());
|
||||
if (exists) {
|
||||
exists.color = tag.color;
|
||||
} else {
|
||||
file.tags.push(tag);
|
||||
}
|
||||
const globalExists = window.globalTags.find(t => t.name.toLowerCase() === tag.name.toLowerCase());
|
||||
if (!globalExists) {
|
||||
window.globalTags.push(tag);
|
||||
localStorage.setItem('globalTags', JSON.stringify(window.globalTags));
|
||||
}
|
||||
}
|
||||
|
||||
// Update the file row (in table view) to show tag badges.
|
||||
export function updateFileRowTagDisplay(file) {
|
||||
const rows = document.querySelectorAll(`[id^="file-row-${encodeURIComponent(file.name)}"]`);
|
||||
console.log('Updating tags for rows:', rows);
|
||||
rows.forEach(row => {
|
||||
let cell = row.querySelector('.file-name-cell');
|
||||
if (cell) {
|
||||
let badgeContainer = cell.querySelector('.tag-badges');
|
||||
if (!badgeContainer) {
|
||||
badgeContainer = document.createElement('div');
|
||||
badgeContainer.className = 'tag-badges';
|
||||
badgeContainer.style.display = 'inline-block';
|
||||
badgeContainer.style.marginLeft = '5px';
|
||||
cell.appendChild(badgeContainer);
|
||||
}
|
||||
badgeContainer.innerHTML = '';
|
||||
if (file.tags && file.tags.length > 0) {
|
||||
file.tags.forEach(tag => {
|
||||
const badge = document.createElement('span');
|
||||
badge.textContent = tag.name;
|
||||
badge.style.backgroundColor = tag.color;
|
||||
badge.style.color = '#fff';
|
||||
badge.style.padding = '2px 4px';
|
||||
badge.style.marginRight = '2px';
|
||||
badge.style.borderRadius = '3px';
|
||||
badge.style.fontSize = '0.8em';
|
||||
badge.style.verticalAlign = 'middle';
|
||||
badgeContainer.appendChild(badge);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function initTagSearch() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
let tagSearchInput = document.getElementById('tagSearchInput');
|
||||
if (!tagSearchInput) {
|
||||
tagSearchInput = document.createElement('input');
|
||||
tagSearchInput.id = 'tagSearchInput';
|
||||
tagSearchInput.placeholder = 'Filter by tag';
|
||||
tagSearchInput.style.marginLeft = '10px';
|
||||
tagSearchInput.style.padding = '5px';
|
||||
searchInput.parentNode.insertBefore(tagSearchInput, searchInput.nextSibling);
|
||||
tagSearchInput.addEventListener('input', () => {
|
||||
window.currentTagFilter = tagSearchInput.value.trim().toLowerCase();
|
||||
if (window.currentFolder) {
|
||||
renderFileTable(window.currentFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function filterFilesByTag(files) {
|
||||
if (window.currentTagFilter && window.currentTagFilter !== '') {
|
||||
return files.filter(file => {
|
||||
if (file.tags && file.tags.length > 0) {
|
||||
return file.tags.some(tag => tag.name.toLowerCase().includes(window.currentTagFilter));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function updateGlobalTagList() {
|
||||
const dataList = document.getElementById("globalTagList");
|
||||
if (dataList) {
|
||||
dataList.innerHTML = "";
|
||||
window.globalTags.forEach(tag => {
|
||||
const option = document.createElement("option");
|
||||
option.value = tag.name;
|
||||
dataList.appendChild(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function saveFileTags(file, deleteGlobal = false, tagToDelete = null) {
|
||||
const folder = file.folder || "root";
|
||||
const payload = {
|
||||
folder: folder,
|
||||
file: file.name,
|
||||
tags: file.tags
|
||||
};
|
||||
if (deleteGlobal && tagToDelete) {
|
||||
payload.file = "global";
|
||||
payload.deleteGlobal = true;
|
||||
payload.tagToDelete = tagToDelete;
|
||||
}
|
||||
fetch("saveFileTag.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log("Tags saved:", data);
|
||||
if (data.globalTags) {
|
||||
window.globalTags = data.globalTags;
|
||||
localStorage.setItem('globalTags', JSON.stringify(window.globalTags));
|
||||
updateCustomTagDropdown();
|
||||
updateMultiCustomTagDropdown();
|
||||
}
|
||||
} else {
|
||||
console.error("Error saving tags:", data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error saving tags:", err);
|
||||
});
|
||||
}
|
||||
749
js/folderManager.js
Normal file
749
js/folderManager.js
Normal file
@@ -0,0 +1,749 @@
|
||||
// folderManager.js
|
||||
|
||||
import { loadFileList } from './fileManager.js';
|
||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
||||
|
||||
/* ----------------------
|
||||
Helper Functions (Data/State)
|
||||
----------------------*/
|
||||
|
||||
// Formats a folder name for display (e.g. adding indentations).
|
||||
export function formatFolderName(folder) {
|
||||
if (typeof folder !== "string") return "";
|
||||
if (folder.indexOf("/") !== -1) {
|
||||
let parts = folder.split("/");
|
||||
let indent = "";
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
indent += "\u00A0\u00A0\u00A0\u00A0"; // 4 non-breaking spaces per level
|
||||
}
|
||||
return indent + parts[parts.length - 1];
|
||||
} else {
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
|
||||
// Build a tree structure from a flat array of folder paths.
|
||||
function buildFolderTree(folders) {
|
||||
const tree = {};
|
||||
folders.forEach(folderPath => {
|
||||
if (typeof folderPath !== "string") return;
|
||||
const parts = folderPath.split('/');
|
||||
let current = tree;
|
||||
parts.forEach(part => {
|
||||
if (!current[part]) {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part];
|
||||
});
|
||||
});
|
||||
return tree;
|
||||
}
|
||||
|
||||
/* ----------------------
|
||||
Folder Tree State (Save/Load)
|
||||
----------------------*/
|
||||
function loadFolderTreeState() {
|
||||
const state = localStorage.getItem("folderTreeState");
|
||||
return state ? JSON.parse(state) : {};
|
||||
}
|
||||
|
||||
function saveFolderTreeState(state) {
|
||||
localStorage.setItem("folderTreeState", JSON.stringify(state));
|
||||
}
|
||||
|
||||
// Helper for getting the parent folder.
|
||||
function getParentFolder(folder) {
|
||||
if (folder === "root") return "root";
|
||||
const lastSlash = folder.lastIndexOf("/");
|
||||
return lastSlash === -1 ? "root" : folder.substring(0, lastSlash);
|
||||
}
|
||||
|
||||
/* ----------------------
|
||||
Breadcrumb Functions
|
||||
----------------------*/
|
||||
function renderBreadcrumb(normalizedFolder) {
|
||||
if (!normalizedFolder || normalizedFolder === "") return "";
|
||||
const parts = normalizedFolder.split("/");
|
||||
let breadcrumbItems = [];
|
||||
// Use the first segment as the root.
|
||||
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="${parts[0]}">${escapeHTML(parts[0])}</span>`);
|
||||
let cumulative = parts[0];
|
||||
parts.slice(1).forEach(part => {
|
||||
cumulative += "/" + part;
|
||||
breadcrumbItems.push(`<span class="breadcrumb-separator"> / </span>`);
|
||||
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="${cumulative}">${escapeHTML(part)}</span>`);
|
||||
});
|
||||
return breadcrumbItems.join('');
|
||||
}
|
||||
|
||||
function bindBreadcrumbEvents() {
|
||||
const breadcrumbLinks = document.querySelectorAll(".breadcrumb-link");
|
||||
breadcrumbLinks.forEach(link => {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
let folder = this.getAttribute("data-folder");
|
||||
window.currentFolder = folder;
|
||||
localStorage.setItem("lastOpenedFolder", folder);
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(folder) + ")";
|
||||
expandTreePath(folder);
|
||||
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
||||
if (targetOption) targetOption.classList.add("selected");
|
||||
loadFileList(folder);
|
||||
bindBreadcrumbEvents();
|
||||
});
|
||||
link.addEventListener("dragover", function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.add("drop-hover");
|
||||
});
|
||||
link.addEventListener("dragleave", function (e) {
|
||||
this.classList.remove("drop-hover");
|
||||
});
|
||||
link.addEventListener("drop", function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove("drop-hover");
|
||||
const dropFolder = this.getAttribute("data-folder");
|
||||
let dragData;
|
||||
try {
|
||||
dragData = JSON.parse(e.dataTransfer.getData("application/json"));
|
||||
} catch (err) {
|
||||
console.error("Invalid drag data on breadcrumb:", err);
|
||||
return;
|
||||
}
|
||||
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
|
||||
if (filesToMove.length === 0) return;
|
||||
fetch("moveFiles.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
source: dragData.sourceFolder,
|
||||
files: filesToMove,
|
||||
destination: dropFolder
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(`File(s) moved successfully to ${dropFolder}!`);
|
||||
loadFileList(dragData.sourceFolder);
|
||||
} else {
|
||||
showToast("Error moving files: " + (data.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error moving files via drop on breadcrumb:", error);
|
||||
showToast("Error moving files.");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ----------------------
|
||||
Check Current User's Folder-Only Permission
|
||||
----------------------*/
|
||||
// This function uses localStorage values (set during login) to determine if the current user is restricted.
|
||||
// If folderOnly is "true", then the personal folder (i.e. username) is forced as the effective root.
|
||||
function checkUserFolderPermission() {
|
||||
const username = localStorage.getItem("username");
|
||||
console.log("checkUserFolderPermission: username =", username);
|
||||
if (!username) {
|
||||
console.warn("No username in localStorage; skipping getUserPermissions fetch.");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (localStorage.getItem("folderOnly") === "true") {
|
||||
window.userFolderOnly = true;
|
||||
console.log("checkUserFolderPermission: using localStorage.folderOnly = true");
|
||||
localStorage.setItem("lastOpenedFolder", username);
|
||||
window.currentFolder = username;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return fetch("getUserPermissions.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(permissionsData => {
|
||||
console.log("checkUserFolderPermission: permissionsData =", permissionsData);
|
||||
if (permissionsData && permissionsData[username] && permissionsData[username].folderOnly) {
|
||||
window.userFolderOnly = true;
|
||||
localStorage.setItem("folderOnly", "true");
|
||||
localStorage.setItem("lastOpenedFolder", username);
|
||||
window.currentFolder = username;
|
||||
return true;
|
||||
} else {
|
||||
window.userFolderOnly = false;
|
||||
localStorage.setItem("folderOnly", "false");
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error fetching user permissions:", err);
|
||||
window.userFolderOnly = false;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/* ----------------------
|
||||
DOM Building Functions for Folder Tree
|
||||
----------------------*/
|
||||
function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
|
||||
const state = loadFolderTreeState();
|
||||
let html = `<ul class="folder-tree ${defaultDisplay === 'none' ? 'collapsed' : 'expanded'}">`;
|
||||
for (const folder in tree) {
|
||||
if (folder.toLowerCase() === "trash") continue;
|
||||
const fullPath = parentPath ? parentPath + "/" + folder : folder;
|
||||
const hasChildren = Object.keys(tree[folder]).length > 0;
|
||||
const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay;
|
||||
html += `<li class="folder-item">`;
|
||||
if (hasChildren) {
|
||||
const toggleSymbol = (displayState === 'none') ? '[+]' : '[' + '<span class="custom-dash">-</span>' + ']';
|
||||
html += `<span class="folder-toggle" data-folder="${fullPath}">${toggleSymbol}</span>`;
|
||||
} else {
|
||||
html += `<span class="folder-indent-placeholder"></span>`;
|
||||
}
|
||||
html += `<span class="folder-option" data-folder="${fullPath}">${escapeHTML(folder)}</span>`;
|
||||
if (hasChildren) {
|
||||
html += renderFolderTree(tree[folder], fullPath, displayState);
|
||||
}
|
||||
html += `</li>`;
|
||||
}
|
||||
html += `</ul>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
function expandTreePath(path) {
|
||||
const parts = path.split("/");
|
||||
let cumulative = "";
|
||||
parts.forEach((part, index) => {
|
||||
cumulative = index === 0 ? part : cumulative + "/" + part;
|
||||
const option = document.querySelector(`.folder-option[data-folder="${cumulative}"]`);
|
||||
if (option) {
|
||||
const li = option.parentNode;
|
||||
const nestedUl = li.querySelector("ul");
|
||||
if (nestedUl && (nestedUl.classList.contains("collapsed") || !nestedUl.classList.contains("expanded"))) {
|
||||
nestedUl.classList.remove("collapsed");
|
||||
nestedUl.classList.add("expanded");
|
||||
const toggle = li.querySelector(".folder-toggle");
|
||||
if (toggle) {
|
||||
toggle.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
|
||||
let state = loadFolderTreeState();
|
||||
state[cumulative] = "block";
|
||||
saveFolderTreeState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ----------------------
|
||||
Drag & Drop Support for Folder Tree Nodes
|
||||
----------------------*/
|
||||
function folderDragOverHandler(event) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.classList.add("drop-hover");
|
||||
}
|
||||
|
||||
function folderDragLeaveHandler(event) {
|
||||
event.currentTarget.classList.remove("drop-hover");
|
||||
}
|
||||
|
||||
function folderDropHandler(event) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.classList.remove("drop-hover");
|
||||
const dropFolder = event.currentTarget.getAttribute("data-folder");
|
||||
let dragData;
|
||||
try {
|
||||
dragData = JSON.parse(event.dataTransfer.getData("application/json"));
|
||||
} catch (e) {
|
||||
console.error("Invalid drag data", e);
|
||||
return;
|
||||
}
|
||||
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
|
||||
if (filesToMove.length === 0) return;
|
||||
fetch("moveFiles.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content")
|
||||
},
|
||||
body: JSON.stringify({
|
||||
source: dragData.sourceFolder,
|
||||
files: filesToMove,
|
||||
destination: dropFolder
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(`File(s) moved successfully to ${dropFolder}!`);
|
||||
loadFileList(dragData.sourceFolder);
|
||||
} else {
|
||||
showToast("Error moving files: " + (data.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error moving files via drop:", error);
|
||||
showToast("Error moving files.");
|
||||
});
|
||||
}
|
||||
|
||||
/* ----------------------
|
||||
Main Folder Tree Rendering and Event Binding
|
||||
----------------------*/
|
||||
export async function loadFolderTree(selectedFolder) {
|
||||
try {
|
||||
// Check if the user has folder-only permission.
|
||||
await checkUserFolderPermission();
|
||||
|
||||
// Determine effective root folder.
|
||||
const username = localStorage.getItem("username") || "root";
|
||||
let effectiveRoot = "root";
|
||||
let effectiveLabel = "(Root)";
|
||||
if (window.userFolderOnly) {
|
||||
effectiveRoot = username; // Use the username as the personal root.
|
||||
effectiveLabel = `(Root)`;
|
||||
// Force override of any saved folder.
|
||||
localStorage.setItem("lastOpenedFolder", username);
|
||||
window.currentFolder = username;
|
||||
} else {
|
||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
||||
}
|
||||
|
||||
// Build fetch URL.
|
||||
let fetchUrl = 'getFolderList.php';
|
||||
if (window.userFolderOnly) {
|
||||
fetchUrl += '?restricted=1';
|
||||
}
|
||||
console.log("Fetching folder list from:", fetchUrl);
|
||||
|
||||
// Fetch folder list from the server.
|
||||
const response = await fetch(fetchUrl);
|
||||
if (response.status === 401) {
|
||||
console.error("Unauthorized: Please log in to view folders.");
|
||||
showToast("Session expired. Please log in again.");
|
||||
window.location.href = "logout.php";
|
||||
return;
|
||||
}
|
||||
let folderData = await response.json();
|
||||
console.log("Folder data received:", folderData);
|
||||
let folders = [];
|
||||
if (Array.isArray(folderData) && folderData.length && typeof folderData[0] === "object" && folderData[0].folder) {
|
||||
folders = folderData.map(item => item.folder);
|
||||
} else if (Array.isArray(folderData)) {
|
||||
folders = folderData;
|
||||
}
|
||||
|
||||
// Remove any global "root" entry.
|
||||
folders = folders.filter(folder => folder.toLowerCase() !== "root");
|
||||
|
||||
// If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself).
|
||||
if (window.userFolderOnly && effectiveRoot !== "root") {
|
||||
folders = folders.filter(folder => folder.startsWith(effectiveRoot + "/"));
|
||||
// Force current folder to be the effective root.
|
||||
localStorage.setItem("lastOpenedFolder", effectiveRoot);
|
||||
window.currentFolder = effectiveRoot;
|
||||
}
|
||||
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
// Render the folder tree.
|
||||
const container = document.getElementById("folderTreeContainer");
|
||||
if (!container) {
|
||||
console.error("Folder tree container not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<div id="rootRow" class="root-row">
|
||||
<span class="folder-toggle" data-folder="${effectiveRoot}">[<span class="custom-dash">-</span>]</span>
|
||||
<span class="folder-option root-folder-option" data-folder="${effectiveRoot}">${effectiveLabel}</span>
|
||||
</div>`;
|
||||
if (folders.length > 0) {
|
||||
const tree = buildFolderTree(folders);
|
||||
html += renderFolderTree(tree, "", "block");
|
||||
}
|
||||
container.innerHTML = html;
|
||||
|
||||
// Attach drag/drop event listeners.
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
el.addEventListener("dragover", folderDragOverHandler);
|
||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||
el.addEventListener("drop", folderDropHandler);
|
||||
});
|
||||
|
||||
if (selectedFolder) {
|
||||
window.currentFolder = selectedFolder;
|
||||
}
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(window.currentFolder) + ")";
|
||||
bindBreadcrumbEvents();
|
||||
loadFileList(window.currentFolder);
|
||||
|
||||
const folderState = loadFolderTreeState();
|
||||
if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") {
|
||||
expandTreePath(window.currentFolder);
|
||||
}
|
||||
|
||||
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
||||
if (selectedEl) {
|
||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
selectedEl.classList.add("selected");
|
||||
}
|
||||
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
el.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
this.classList.add("selected");
|
||||
const selected = this.getAttribute("data-folder");
|
||||
window.currentFolder = selected;
|
||||
localStorage.setItem("lastOpenedFolder", selected);
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
titleEl.innerHTML = "Files in (" + renderBreadcrumb(selected) + ")";
|
||||
bindBreadcrumbEvents();
|
||||
loadFileList(selected);
|
||||
});
|
||||
});
|
||||
|
||||
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
||||
if (rootToggle) {
|
||||
rootToggle.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
const nestedUl = container.querySelector("#rootRow + ul");
|
||||
if (nestedUl) {
|
||||
let state = loadFolderTreeState();
|
||||
if (nestedUl.classList.contains("collapsed") || !nestedUl.classList.contains("expanded")) {
|
||||
nestedUl.classList.remove("collapsed");
|
||||
nestedUl.classList.add("expanded");
|
||||
this.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
|
||||
state[effectiveRoot] = "block";
|
||||
} else {
|
||||
nestedUl.classList.remove("expanded");
|
||||
nestedUl.classList.add("collapsed");
|
||||
this.textContent = "[+]";
|
||||
state[effectiveRoot] = "none";
|
||||
}
|
||||
saveFolderTreeState(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
container.querySelectorAll(".folder-toggle").forEach(toggle => {
|
||||
toggle.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
const siblingUl = this.parentNode.querySelector("ul");
|
||||
const folderPath = this.getAttribute("data-folder");
|
||||
let state = loadFolderTreeState();
|
||||
if (siblingUl) {
|
||||
if (siblingUl.classList.contains("collapsed") || !siblingUl.classList.contains("expanded")) {
|
||||
siblingUl.classList.remove("collapsed");
|
||||
siblingUl.classList.add("expanded");
|
||||
this.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
|
||||
state[folderPath] = "block";
|
||||
} else {
|
||||
siblingUl.classList.remove("expanded");
|
||||
siblingUl.classList.add("collapsed");
|
||||
this.textContent = "[+]";
|
||||
state[folderPath] = "none";
|
||||
}
|
||||
saveFolderTreeState(state);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading folder tree:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// For backward compatibility.
|
||||
export function loadFolderList(selectedFolder) {
|
||||
loadFolderTree(selectedFolder);
|
||||
}
|
||||
|
||||
/* ----------------------
|
||||
Folder Management (Rename, Delete, Create)
|
||||
----------------------*/
|
||||
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
|
||||
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
|
||||
|
||||
function openRenameFolderModal() {
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
if (!selectedFolder || selectedFolder === "root") {
|
||||
showToast("Please select a valid folder to rename.");
|
||||
return;
|
||||
}
|
||||
const parts = selectedFolder.split("/");
|
||||
document.getElementById("newRenameFolderName").value = parts[parts.length - 1];
|
||||
document.getElementById("renameFolderModal").style.display = "block";
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById("newRenameFolderName");
|
||||
input.focus();
|
||||
input.select();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
document.getElementById("cancelRenameFolder").addEventListener("click", function () {
|
||||
document.getElementById("renameFolderModal").style.display = "none";
|
||||
document.getElementById("newRenameFolderName").value = "";
|
||||
});
|
||||
attachEnterKeyListener("renameFolderModal", "submitRenameFolder");
|
||||
document.getElementById("submitRenameFolder").addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
const newNameBasename = document.getElementById("newRenameFolderName").value.trim();
|
||||
if (!newNameBasename || newNameBasename === selectedFolder.split("/").pop()) {
|
||||
showToast("Please enter a valid new folder name.");
|
||||
return;
|
||||
}
|
||||
const parentPath = getParentFolder(selectedFolder);
|
||||
const newFolderFull = parentPath === "root" ? newNameBasename : parentPath + "/" + newNameBasename;
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
if (!csrfToken) {
|
||||
showToast("CSRF token not loaded yet! Please try again.");
|
||||
return;
|
||||
}
|
||||
fetch("renameFolder.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({ oldFolder: window.currentFolder, newFolder: newFolderFull })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("Folder renamed successfully!");
|
||||
window.currentFolder = newFolderFull;
|
||||
localStorage.setItem("lastOpenedFolder", newFolderFull);
|
||||
loadFolderList(newFolderFull);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not rename folder"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error renaming folder:", error))
|
||||
.finally(() => {
|
||||
document.getElementById("renameFolderModal").style.display = "none";
|
||||
document.getElementById("newRenameFolderName").value = "";
|
||||
});
|
||||
});
|
||||
|
||||
function openDeleteFolderModal() {
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
if (!selectedFolder || selectedFolder === "root") {
|
||||
showToast("Please select a valid folder to delete.");
|
||||
return;
|
||||
}
|
||||
document.getElementById("deleteFolderMessage").textContent =
|
||||
"Are you sure you want to delete folder " + selectedFolder + "?";
|
||||
document.getElementById("deleteFolderModal").style.display = "block";
|
||||
}
|
||||
|
||||
document.getElementById("cancelDeleteFolder").addEventListener("click", function () {
|
||||
document.getElementById("deleteFolderModal").style.display = "none";
|
||||
});
|
||||
attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder");
|
||||
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
fetch("deleteFolder.php", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({ folder: selectedFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("Folder deleted successfully!");
|
||||
window.currentFolder = getParentFolder(selectedFolder);
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
loadFolderList(window.currentFolder);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not delete folder"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error deleting folder:", error))
|
||||
.finally(() => {
|
||||
document.getElementById("deleteFolderModal").style.display = "none";
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("createFolderBtn").addEventListener("click", function () {
|
||||
document.getElementById("createFolderModal").style.display = "block";
|
||||
document.getElementById("newFolderName").focus();
|
||||
});
|
||||
|
||||
document.getElementById("cancelCreateFolder").addEventListener("click", function () {
|
||||
document.getElementById("createFolderModal").style.display = "none";
|
||||
document.getElementById("newFolderName").value = "";
|
||||
});
|
||||
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
|
||||
document.getElementById("submitCreateFolder").addEventListener("click", function () {
|
||||
const folderInput = document.getElementById("newFolderName").value.trim();
|
||||
if (!folderInput) {
|
||||
showToast("Please enter a folder name.");
|
||||
return;
|
||||
}
|
||||
let selectedFolder = window.currentFolder || "root";
|
||||
let fullFolderName = folderInput;
|
||||
if (selectedFolder && selectedFolder !== "root") {
|
||||
fullFolderName = selectedFolder + "/" + folderInput;
|
||||
}
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
fetch("createFolder.php", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folderName: folderInput,
|
||||
parent: selectedFolder === "root" ? "" : selectedFolder
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("Folder created successfully!");
|
||||
window.currentFolder = fullFolderName;
|
||||
localStorage.setItem("lastOpenedFolder", fullFolderName);
|
||||
loadFolderList(fullFolderName);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not create folder"));
|
||||
}
|
||||
document.getElementById("createFolderModal").style.display = "none";
|
||||
document.getElementById("newFolderName").value = "";
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error creating folder:", error);
|
||||
document.getElementById("createFolderModal").style.display = "none";
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ----------
|
||||
function showFolderManagerContextMenu(x, y, menuItems) {
|
||||
let menu = document.getElementById("folderManagerContextMenu");
|
||||
if (!menu) {
|
||||
menu = document.createElement("div");
|
||||
menu.id = "folderManagerContextMenu";
|
||||
menu.style.position = "absolute";
|
||||
menu.style.padding = "5px 0";
|
||||
menu.style.minWidth = "150px";
|
||||
menu.style.zIndex = "9999";
|
||||
document.body.appendChild(menu);
|
||||
}
|
||||
if (document.body.classList.contains("dark-mode")) {
|
||||
menu.style.backgroundColor = "#2c2c2c";
|
||||
menu.style.border = "1px solid #555";
|
||||
menu.style.color = "#e0e0e0";
|
||||
} else {
|
||||
menu.style.backgroundColor = "#fff";
|
||||
menu.style.border = "1px solid #ccc";
|
||||
menu.style.color = "#000";
|
||||
}
|
||||
menu.innerHTML = "";
|
||||
menuItems.forEach(item => {
|
||||
const menuItem = document.createElement("div");
|
||||
menuItem.textContent = item.label;
|
||||
menuItem.style.padding = "5px 15px";
|
||||
menuItem.style.cursor = "pointer";
|
||||
menuItem.addEventListener("mouseover", () => {
|
||||
if (document.body.classList.contains("dark-mode")) {
|
||||
menuItem.style.backgroundColor = "#444";
|
||||
} else {
|
||||
menuItem.style.backgroundColor = "#f0f0f0";
|
||||
}
|
||||
});
|
||||
menuItem.addEventListener("mouseout", () => {
|
||||
menuItem.style.backgroundColor = "";
|
||||
});
|
||||
menuItem.addEventListener("click", () => {
|
||||
item.action();
|
||||
hideFolderManagerContextMenu();
|
||||
});
|
||||
menu.appendChild(menuItem);
|
||||
});
|
||||
menu.style.left = x + "px";
|
||||
menu.style.top = y + "px";
|
||||
menu.style.display = "block";
|
||||
}
|
||||
|
||||
function hideFolderManagerContextMenu() {
|
||||
const menu = document.getElementById("folderManagerContextMenu");
|
||||
if (menu) {
|
||||
menu.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function folderManagerContextMenuHandler(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const target = e.target.closest(".folder-option, .breadcrumb-link");
|
||||
if (!target) return;
|
||||
const folder = target.getAttribute("data-folder");
|
||||
if (!folder) return;
|
||||
window.currentFolder = folder;
|
||||
document.querySelectorAll(".folder-option, .breadcrumb-link").forEach(el => el.classList.remove("selected"));
|
||||
target.classList.add("selected");
|
||||
const menuItems = [
|
||||
{
|
||||
label: "Create Folder",
|
||||
action: () => {
|
||||
document.getElementById("createFolderModal").style.display = "block";
|
||||
document.getElementById("newFolderName").focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Rename Folder",
|
||||
action: () => { openRenameFolderModal(); }
|
||||
},
|
||||
{
|
||||
label: "Delete Folder",
|
||||
action: () => { openDeleteFolderModal(); }
|
||||
}
|
||||
];
|
||||
showFolderManagerContextMenu(e.pageX, e.pageY, menuItems);
|
||||
}
|
||||
|
||||
function bindFolderManagerContextMenu() {
|
||||
const container = document.getElementById("folderTreeContainer");
|
||||
if (container) {
|
||||
container.removeEventListener("contextmenu", folderManagerContextMenuHandler);
|
||||
container.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
|
||||
}
|
||||
const breadcrumbNodes = document.querySelectorAll(".breadcrumb-link");
|
||||
breadcrumbNodes.forEach(node => {
|
||||
node.removeEventListener("contextmenu", folderManagerContextMenuHandler);
|
||||
node.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("click", function () {
|
||||
hideFolderManagerContextMenu();
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.addEventListener("keydown", function (e) {
|
||||
const tag = e.target.tagName.toLowerCase();
|
||||
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) {
|
||||
if (window.currentFolder && window.currentFolder !== "root") {
|
||||
e.preventDefault();
|
||||
openDeleteFolderModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
bindFolderManagerContextMenu();
|
||||
191
js/main.js
Normal file
191
js/main.js
Normal file
@@ -0,0 +1,191 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import {
|
||||
toggleVisibility,
|
||||
toggleAllCheckboxes,
|
||||
updateFileActionButtons,
|
||||
showToast
|
||||
} from './domUtils.js';
|
||||
import {
|
||||
loadFileList,
|
||||
initFileActions,
|
||||
editFile,
|
||||
saveFile,
|
||||
displayFilePreview,
|
||||
renameFile
|
||||
} from './fileManager.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth, checkAuthentication } from './auth.js';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||
|
||||
function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
|
||||
return fetch('token.php', { credentials: 'include' })
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Token fetch failed with status: " + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Set global variables.
|
||||
window.csrfToken = data.csrf_token;
|
||||
window.SHARE_URL = data.share_url;
|
||||
|
||||
// Update (or create) the CSRF meta tag.
|
||||
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!metaCSRF) {
|
||||
metaCSRF = document.createElement('meta');
|
||||
metaCSRF.name = 'csrf-token';
|
||||
document.head.appendChild(metaCSRF);
|
||||
}
|
||||
metaCSRF.setAttribute('content', data.csrf_token);
|
||||
|
||||
// Update (or create) the share URL meta tag.
|
||||
let metaShare = document.querySelector('meta[name="share-url"]');
|
||||
if (!metaShare) {
|
||||
metaShare = document.createElement('meta');
|
||||
metaShare.name = 'share-url';
|
||||
document.head.appendChild(metaShare);
|
||||
}
|
||||
metaShare.setAttribute('content', data.share_url);
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch(error => {
|
||||
if (retries > 0) {
|
||||
console.warn(`CSRF token load failed. Retrying in ${delay}ms... (${retries} retries left)`, error);
|
||||
return new Promise(resolve => setTimeout(resolve, delay))
|
||||
.then(() => loadCsrfTokenWithRetry(retries - 1, delay * 2));
|
||||
}
|
||||
console.error("Failed to load CSRF token after retries.", error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
// Expose functions for inline handlers.
|
||||
window.sendRequest = sendRequest;
|
||||
window.toggleVisibility = toggleVisibility;
|
||||
window.toggleAllCheckboxes = toggleAllCheckboxes;
|
||||
window.editFile = editFile;
|
||||
window.saveFile = saveFile;
|
||||
window.renameFile = renameFile;
|
||||
|
||||
// Global variable for the current folder.
|
||||
window.currentFolder = "root";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// First, load the CSRF token (with retry).
|
||||
loadCsrfTokenWithRetry().then(() => {
|
||||
// Once CSRF token is loaded, initialize authentication.
|
||||
initAuth();
|
||||
|
||||
// Continue with initializations that rely on a valid CSRF token:
|
||||
checkAuthentication().then(authenticated => {
|
||||
if (authenticated) {
|
||||
window.currentFolder = "root";
|
||||
initTagSearch();
|
||||
loadFileList(window.currentFolder);
|
||||
initDragAndDrop();
|
||||
loadSidebarOrder();
|
||||
loadHeaderOrder();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderTree();
|
||||
setupTrashRestoreDelete();
|
||||
|
||||
const helpBtn = document.getElementById("folderHelpBtn");
|
||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||
helpBtn.addEventListener("click", function () {
|
||||
// Toggle display of the tooltip.
|
||||
if (helpTooltip.style.display === "none" || helpTooltip.style.display === "") {
|
||||
helpTooltip.style.display = "block";
|
||||
} else {
|
||||
helpTooltip.style.display = "none";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn("User not authenticated. Data loading deferred.");
|
||||
}
|
||||
});
|
||||
|
||||
// Other DOM initialization that can happen after CSRF is ready.
|
||||
const newPasswordInput = document.getElementById("newPassword");
|
||||
if (newPasswordInput) {
|
||||
newPasswordInput.addEventListener("input", function() {
|
||||
console.log("newPassword input event:", this.value);
|
||||
});
|
||||
} else {
|
||||
console.error("newPassword input not found!");
|
||||
}
|
||||
|
||||
// --- Dark Mode Persistence ---
|
||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||
const storedDarkMode = localStorage.getItem("darkMode");
|
||||
|
||||
if (storedDarkMode === "true") {
|
||||
document.body.classList.add("dark-mode");
|
||||
} else if (storedDarkMode === "false") {
|
||||
document.body.classList.remove("dark-mode");
|
||||
} else {
|
||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
document.body.classList.add("dark-mode");
|
||||
} else {
|
||||
document.body.classList.remove("dark-mode");
|
||||
}
|
||||
}
|
||||
|
||||
if (darkModeToggle) {
|
||||
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
|
||||
? "Light Mode"
|
||||
: "Dark Mode";
|
||||
|
||||
darkModeToggle.addEventListener("click", function () {
|
||||
if (document.body.classList.contains("dark-mode")) {
|
||||
document.body.classList.remove("dark-mode");
|
||||
localStorage.setItem("darkMode", "false");
|
||||
darkModeToggle.textContent = "Dark Mode";
|
||||
} else {
|
||||
document.body.classList.add("dark-mode");
|
||||
localStorage.setItem("darkMode", "true");
|
||||
darkModeToggle.textContent = "Light Mode";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (localStorage.getItem("darkMode") === null && window.matchMedia) {
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => {
|
||||
if (event.matches) {
|
||||
document.body.classList.add("dark-mode");
|
||||
if (darkModeToggle) darkModeToggle.textContent = "Light Mode";
|
||||
} else {
|
||||
document.body.classList.remove("dark-mode");
|
||||
if (darkModeToggle) darkModeToggle.textContent = "Dark Mode";
|
||||
}
|
||||
});
|
||||
}
|
||||
// --- End Dark Mode Persistence ---
|
||||
|
||||
const message = sessionStorage.getItem("welcomeMessage");
|
||||
if (message) {
|
||||
showToast(message);
|
||||
sessionStorage.removeItem("welcomeMessage");
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Initialization halted due to CSRF token load failure.", error);
|
||||
});
|
||||
|
||||
// --- Auto-scroll During Drag ---
|
||||
// Adjust these values as needed:
|
||||
const SCROLL_THRESHOLD = 50; // pixels from edge to start scrolling
|
||||
const SCROLL_SPEED = 20; // pixels to scroll per event
|
||||
|
||||
document.addEventListener("dragover", function (e) {
|
||||
if (e.clientY < SCROLL_THRESHOLD) {
|
||||
window.scrollBy(0, -SCROLL_SPEED);
|
||||
} else if (e.clientY > window.innerHeight - SCROLL_THRESHOLD) {
|
||||
window.scrollBy(0, SCROLL_SPEED);
|
||||
}
|
||||
});
|
||||
});
|
||||
31
js/networkUtils.js
Normal file
31
js/networkUtils.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export function sendRequest(url, method = "GET", data = null, customHeaders = {}) {
|
||||
const options = {
|
||||
method,
|
||||
credentials: 'include',
|
||||
headers: {}
|
||||
};
|
||||
|
||||
// Merge custom headers
|
||||
Object.assign(options.headers, customHeaders);
|
||||
|
||||
// If data is provided and is not FormData, assume JSON.
|
||||
if (data && !(data instanceof FormData)) {
|
||||
if (!options.headers["Content-Type"]) {
|
||||
options.headers["Content-Type"] = "application/json";
|
||||
}
|
||||
options.body = JSON.stringify(data);
|
||||
} else if (data instanceof FormData) {
|
||||
options.body = data;
|
||||
}
|
||||
|
||||
return fetch(url, options)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`HTTP error ${response.status}: ${text}`);
|
||||
});
|
||||
}
|
||||
const clonedResponse = response.clone();
|
||||
return response.json().catch(() => clonedResponse.text());
|
||||
});
|
||||
}
|
||||
307
js/trashRestoreDelete.js
Normal file
307
js/trashRestoreDelete.js
Normal file
@@ -0,0 +1,307 @@
|
||||
// trashRestoreDelete.js
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, showToast } from './domUtils.js';
|
||||
import { loadFileList } from './fileManager.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
|
||||
function showConfirm(message, onConfirm) {
|
||||
const modal = document.getElementById("customConfirmModal");
|
||||
const messageElem = document.getElementById("confirmMessage");
|
||||
const yesBtn = document.getElementById("confirmYesBtn");
|
||||
const noBtn = document.getElementById("confirmNoBtn");
|
||||
|
||||
if (!modal || !messageElem || !yesBtn || !noBtn) {
|
||||
if (confirm(message)) {
|
||||
onConfirm();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
messageElem.textContent = message;
|
||||
modal.style.display = "block";
|
||||
|
||||
// Clear any previous event listeners by cloning the node.
|
||||
const yesBtnClone = yesBtn.cloneNode(true);
|
||||
yesBtn.parentNode.replaceChild(yesBtnClone, yesBtn);
|
||||
const noBtnClone = noBtn.cloneNode(true);
|
||||
noBtn.parentNode.replaceChild(noBtnClone, noBtn);
|
||||
|
||||
yesBtnClone.addEventListener("click", () => {
|
||||
modal.style.display = "none";
|
||||
onConfirm();
|
||||
});
|
||||
noBtnClone.addEventListener("click", () => {
|
||||
modal.style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
export function setupTrashRestoreDelete() {
|
||||
|
||||
// --- Attach listener to the restore button (created in auth.js) to open the modal.
|
||||
const restoreBtn = document.getElementById("restoreFilesBtn");
|
||||
if (restoreBtn) {
|
||||
restoreBtn.addEventListener("click", () => {
|
||||
toggleVisibility("restoreFilesModal", true);
|
||||
loadTrashItems();
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
const retryBtn = document.getElementById("restoreFilesBtn");
|
||||
if (retryBtn) {
|
||||
retryBtn.addEventListener("click", () => {
|
||||
toggleVisibility("restoreFilesModal", true);
|
||||
loadTrashItems();
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
||||
// --- Restore Selected: Restore only the selected trash items.
|
||||
const restoreSelectedBtn = document.getElementById("restoreSelectedBtn");
|
||||
if (restoreSelectedBtn) {
|
||||
restoreSelectedBtn.addEventListener("click", () => {
|
||||
const selected = document.querySelectorAll("#restoreFilesList input[type='checkbox']:checked");
|
||||
const files = Array.from(selected).map(chk => chk.value);
|
||||
console.log("Restore Selected clicked, files:", files);
|
||||
if (files.length === 0) {
|
||||
showToast("No trash items selected for restore.");
|
||||
return;
|
||||
}
|
||||
fetch("restoreFiles.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ files })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.success);
|
||||
toggleVisibility("restoreFilesModal", false);
|
||||
loadFileList(window.currentFolder);
|
||||
loadFolderTree(window.currentFolder);
|
||||
} else {
|
||||
showToast(data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error restoring files:", err);
|
||||
showToast("Error restoring files.");
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.error("restoreSelectedBtn not found.");
|
||||
}
|
||||
|
||||
// --- Restore All: Restore all trash items.
|
||||
const restoreAllBtn = document.getElementById("restoreAllBtn");
|
||||
if (restoreAllBtn) {
|
||||
restoreAllBtn.addEventListener("click", () => {
|
||||
const allChk = document.querySelectorAll("#restoreFilesList input[type='checkbox']");
|
||||
const files = Array.from(allChk).map(chk => chk.value);
|
||||
console.log("Restore All clicked, files:", files);
|
||||
if (files.length === 0) {
|
||||
showToast("Trash is empty.");
|
||||
return;
|
||||
}
|
||||
fetch("restoreFiles.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ files })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.success);
|
||||
toggleVisibility("restoreFilesModal", false);
|
||||
loadFileList(window.currentFolder);
|
||||
loadFolderTree(window.currentFolder);
|
||||
|
||||
} else {
|
||||
showToast(data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error restoring files:", err);
|
||||
showToast("Error restoring files.");
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.error("restoreAllBtn not found.");
|
||||
}
|
||||
|
||||
// --- Delete Selected: Permanently delete selected trash items with confirmation.
|
||||
const deleteTrashSelectedBtn = document.getElementById("deleteTrashSelectedBtn");
|
||||
if (deleteTrashSelectedBtn) {
|
||||
deleteTrashSelectedBtn.addEventListener("click", () => {
|
||||
const selected = document.querySelectorAll("#restoreFilesList input[type='checkbox']:checked");
|
||||
const files = Array.from(selected).map(chk => chk.value);
|
||||
console.log("Delete Selected clicked, files:", files);
|
||||
if (files.length === 0) {
|
||||
showToast("No trash items selected for deletion.");
|
||||
return;
|
||||
}
|
||||
showConfirm("Are you sure you want to permanently delete the selected trash items?", () => {
|
||||
fetch("deleteTrashFiles.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ files })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.success);
|
||||
loadTrashItems();
|
||||
loadFileList(window.currentFolder);
|
||||
loadFolderTree(window.currentFolder);
|
||||
} else {
|
||||
showToast(data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error deleting trash files:", err);
|
||||
showToast("Error deleting trash files.");
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.error("deleteTrashSelectedBtn not found.");
|
||||
}
|
||||
|
||||
// --- Delete All: Permanently delete all trash items with confirmation.
|
||||
const deleteAllBtn = document.getElementById("deleteAllBtn");
|
||||
if (deleteAllBtn) {
|
||||
deleteAllBtn.addEventListener("click", () => {
|
||||
showConfirm("Are you sure you want to permanently delete all trash items? This action cannot be undone.", () => {
|
||||
fetch("deleteTrashFiles.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ deleteAll: true })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.success);
|
||||
toggleVisibility("restoreFilesModal", false);
|
||||
loadFileList(window.currentFolder);
|
||||
loadFolderTree(window.currentFolder);
|
||||
} else {
|
||||
showToast(data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error deleting all trash files:", err);
|
||||
showToast("Error deleting all trash files.");
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.error("deleteAllBtn not found.");
|
||||
}
|
||||
|
||||
// --- Close the Restore Modal ---
|
||||
const closeRestoreModal = document.getElementById("closeRestoreModal");
|
||||
if (closeRestoreModal) {
|
||||
closeRestoreModal.addEventListener("click", () => {
|
||||
toggleVisibility("restoreFilesModal", false);
|
||||
});
|
||||
} else {
|
||||
console.error("closeRestoreModal not found.");
|
||||
}
|
||||
|
||||
// --- Auto-purge old trash items (older than 3 days) ---
|
||||
autoPurgeOldTrash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads trash items from the server and updates the restore modal list.
|
||||
*/
|
||||
export function loadTrashItems() {
|
||||
fetch("getTrashItems.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(trashItems => {
|
||||
const listContainer = document.getElementById("restoreFilesList");
|
||||
if (listContainer) {
|
||||
listContainer.innerHTML = "";
|
||||
trashItems.forEach(item => {
|
||||
const li = document.createElement("li");
|
||||
li.style.listStyle = "none";
|
||||
li.style.marginBottom = "5px";
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.value = item.trashName;
|
||||
li.appendChild(checkbox);
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.style.marginLeft = "8px";
|
||||
// Include the deletedBy username in the label text.
|
||||
const deletedBy = item.deletedBy ? item.deletedBy : "Unknown";
|
||||
label.textContent = `${item.originalName} (${deletedBy} trashed on ${new Date(item.trashedAt * 1000).toLocaleString()})`;
|
||||
li.appendChild(label);
|
||||
|
||||
listContainer.appendChild(li);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error loading trash items:", err);
|
||||
showToast("Error loading trash items.");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically purges (permanently deletes) trash items older than 3 days.
|
||||
*/
|
||||
function autoPurgeOldTrash() {
|
||||
fetch("getTrashItems.php", { credentials: "include" })
|
||||
.then(response => response.json())
|
||||
.then(trashItems => {
|
||||
const now = Date.now();
|
||||
const threeDays = 3 * 24 * 60 * 60 * 1000;
|
||||
const oldItems = trashItems.filter(item => (now - (item.trashedAt * 1000)) > threeDays);
|
||||
if (oldItems.length > 0) {
|
||||
const files = oldItems.map(item => item.trashName);
|
||||
fetch("deleteTrashFiles.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ files })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log("Auto-purged old trash items:", data.success);
|
||||
loadTrashItems();
|
||||
} else {
|
||||
console.warn("Auto-purge warning:", data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error auto-purging old trash items:", err);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error retrieving trash items for auto-purge:", err);
|
||||
});
|
||||
}
|
||||
809
js/upload.js
Normal file
809
js/upload.js
Normal file
@@ -0,0 +1,809 @@
|
||||
import { loadFileList, displayFilePreview, initFileActions } from './fileManager.js';
|
||||
import { showToast, escapeHTML } from './domUtils.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
|
||||
/* -----------------------------------------------------
|
||||
Helpers for Drag–and–Drop Folder Uploads (Original Code)
|
||||
----------------------------------------------------- */
|
||||
// Recursively traverse a dropped folder.
|
||||
function traverseFileTreePromise(item, path = "") {
|
||||
return new Promise((resolve) => {
|
||||
if (item.isFile) {
|
||||
item.file(file => {
|
||||
// Store relative path for folder uploads.
|
||||
Object.defineProperty(file, 'customRelativePath', {
|
||||
value: path + file.name,
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
resolve([file]);
|
||||
});
|
||||
} else if (item.isDirectory) {
|
||||
const dirReader = item.createReader();
|
||||
dirReader.readEntries(entries => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
promises.push(traverseFileTreePromise(entries[i], path + item.name + "/"));
|
||||
}
|
||||
Promise.all(promises).then(results => resolve(results.flat()));
|
||||
});
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Recursively retrieve files from DataTransfer items.
|
||||
function getFilesFromDataTransferItems(items) {
|
||||
const promises = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const entry = items[i].webkitGetAsEntry();
|
||||
if (entry) {
|
||||
promises.push(traverseFileTreePromise(entry));
|
||||
}
|
||||
}
|
||||
return Promise.all(promises).then(results => results.flat());
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------
|
||||
UI Helpers (Mostly unchanged from your original code)
|
||||
----------------------------------------------------- */
|
||||
function setDropAreaDefault() {
|
||||
const dropArea = document.getElementById("uploadDropArea");
|
||||
if (dropArea) {
|
||||
dropArea.innerHTML = `
|
||||
<div id="uploadInstruction" class="upload-instruction">
|
||||
Drop files/folders here or click 'Choose files'
|
||||
</div>
|
||||
<div id="uploadFileRow" class="upload-file-row">
|
||||
<button id="customChooseBtn" type="button">Choose files</button>
|
||||
</div>
|
||||
<div id="fileInfoWrapper" class="file-info-wrapper">
|
||||
<div id="fileInfoContainer" class="file-info-container">
|
||||
<span id="fileInfoDefault">No files selected</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- File input for file picker (files only) -->
|
||||
<input type="file" id="file" name="file[]" class="form-control-file" multiple style="opacity:0; position:absolute; width:1px; height:1px;" />
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function adjustFolderHelpExpansion() {
|
||||
const uploadCard = document.getElementById("uploadCard");
|
||||
const folderHelpDetails = document.querySelector(".folder-help-details");
|
||||
if (uploadCard && folderHelpDetails) {
|
||||
if (uploadCard.offsetHeight > 400) {
|
||||
folderHelpDetails.setAttribute("open", "");
|
||||
} else {
|
||||
folderHelpDetails.removeAttribute("open");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function adjustFolderHelpExpansionClosed() {
|
||||
const folderHelpDetails = document.querySelector(".folder-help-details");
|
||||
if (folderHelpDetails) {
|
||||
folderHelpDetails.removeAttribute("open");
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileInfoCount() {
|
||||
const fileInfoContainer = document.getElementById("fileInfoContainer");
|
||||
if (fileInfoContainer && window.selectedFiles) {
|
||||
if (window.selectedFiles.length === 0) {
|
||||
fileInfoContainer.innerHTML = `<span id="fileInfoDefault">No files selected</span>`;
|
||||
} else if (window.selectedFiles.length === 1) {
|
||||
fileInfoContainer.innerHTML = `
|
||||
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;">
|
||||
<span class="material-icons file-icon">insert_drive_file</span>
|
||||
</div>
|
||||
<span id="fileNameDisplay" class="file-name-display">${escapeHTML(window.selectedFiles[0].name || window.selectedFiles[0].fileName || "Unnamed File")}</span>
|
||||
`;
|
||||
} else {
|
||||
fileInfoContainer.innerHTML = `
|
||||
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;">
|
||||
<span class="material-icons file-icon">insert_drive_file</span>
|
||||
</div>
|
||||
<span id="fileCountDisplay" class="file-name-display">${window.selectedFiles.length} files selected</span>
|
||||
`;
|
||||
}
|
||||
const previewContainer = document.getElementById("filePreviewContainer");
|
||||
if (previewContainer && window.selectedFiles.length > 0) {
|
||||
previewContainer.innerHTML = "";
|
||||
// For image files, try to show a preview (if available from the file object).
|
||||
displayFilePreview(window.selectedFiles[0].file || window.selectedFiles[0], previewContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to repeatedly call removeChunks.php
|
||||
function removeChunkFolderRepeatedly(identifier, csrfToken, maxAttempts = 3, interval = 1000) {
|
||||
let attempt = 0;
|
||||
const removalInterval = setInterval(() => {
|
||||
attempt++;
|
||||
const params = new URLSearchParams();
|
||||
// Prefix with "resumable_" to match your PHP regex.
|
||||
params.append('folder', 'resumable_' + identifier);
|
||||
params.append('csrf_token', csrfToken);
|
||||
fetch('removeChunks.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: params.toString()
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(`Chunk folder removal attempt ${attempt}:`, data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`Error on removal attempt ${attempt}:`, err);
|
||||
});
|
||||
if (attempt >= maxAttempts) {
|
||||
clearInterval(removalInterval);
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------
|
||||
File Entry Creation (with Pause/Resume and Restart)
|
||||
----------------------------------------------------- */
|
||||
// Create a file entry element with a remove button and a pause/resume button.
|
||||
function createFileEntry(file) {
|
||||
const li = document.createElement("li");
|
||||
li.classList.add("upload-progress-item");
|
||||
li.style.display = "flex";
|
||||
li.dataset.uploadIndex = file.uploadIndex;
|
||||
|
||||
// Remove button (always added)
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.classList.add("remove-file-btn");
|
||||
removeBtn.textContent = "×";
|
||||
// In your remove button event listener, replace the fetch call with:
|
||||
removeBtn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
const uploadIndex = file.uploadIndex;
|
||||
window.selectedFiles = window.selectedFiles.filter(f => f.uploadIndex !== uploadIndex);
|
||||
|
||||
// Cancel the file upload if possible.
|
||||
if (typeof file.cancel === "function") {
|
||||
file.cancel();
|
||||
console.log("Canceled file upload:", file.fileName);
|
||||
}
|
||||
|
||||
// Remove file from the resumable queue.
|
||||
if (resumableInstance && typeof resumableInstance.removeFile === "function") {
|
||||
resumableInstance.removeFile(file);
|
||||
}
|
||||
|
||||
// Call our helper repeatedly to remove the chunk folder.
|
||||
if (file.uniqueIdentifier) {
|
||||
removeChunkFolderRepeatedly(file.uniqueIdentifier, window.csrfToken, 3, 1000);
|
||||
}
|
||||
|
||||
li.remove();
|
||||
updateFileInfoCount();
|
||||
});
|
||||
li.removeBtn = removeBtn;
|
||||
li.appendChild(removeBtn);
|
||||
|
||||
// Add pause/resume/restart button if the file supports pause/resume.
|
||||
// Conditionally add the pause/resume button only if file.pause is available
|
||||
// Pause/Resume button (for resumable file–picker uploads)
|
||||
if (typeof file.pause === "function") {
|
||||
const pauseResumeBtn = document.createElement("button");
|
||||
pauseResumeBtn.setAttribute("type", "button"); // not a submit button
|
||||
pauseResumeBtn.classList.add("pause-resume-btn");
|
||||
// Start with pause icon and disable button until upload starts
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
|
||||
pauseResumeBtn.disabled = true;
|
||||
pauseResumeBtn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
if (file.isError) {
|
||||
// If the file previously failed, try restarting upload.
|
||||
if (typeof file.retry === "function") {
|
||||
file.retry();
|
||||
file.isError = false;
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
|
||||
}
|
||||
} else if (!file.paused) {
|
||||
// Pause the upload (if possible)
|
||||
if (typeof file.pause === "function") {
|
||||
file.pause();
|
||||
file.paused = true;
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">play_circle_outline</span>';
|
||||
} else {
|
||||
}
|
||||
} else if (file.paused) {
|
||||
// Resume sequence: first call to resume (or upload() fallback)
|
||||
if (typeof file.resume === "function") {
|
||||
file.resume();
|
||||
} else {
|
||||
resumableInstance.upload();
|
||||
}
|
||||
// After a short delay, pause again then resume
|
||||
setTimeout(() => {
|
||||
if (typeof file.pause === "function") {
|
||||
file.pause();
|
||||
} else {
|
||||
resumableInstance.upload();
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (typeof file.resume === "function") {
|
||||
file.resume();
|
||||
} else {
|
||||
resumableInstance.upload();
|
||||
}
|
||||
}, 100);
|
||||
}, 100);
|
||||
file.paused = false;
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
|
||||
} else {
|
||||
console.error("Pause/resume function not available for file", file);
|
||||
}
|
||||
});
|
||||
li.appendChild(pauseResumeBtn);
|
||||
}
|
||||
|
||||
// Preview element
|
||||
const preview = document.createElement("div");
|
||||
preview.className = "file-preview";
|
||||
displayFilePreview(file, preview);
|
||||
li.appendChild(preview);
|
||||
|
||||
// File name display
|
||||
const nameDiv = document.createElement("div");
|
||||
nameDiv.classList.add("upload-file-name");
|
||||
nameDiv.textContent = file.name || file.fileName || "Unnamed File";
|
||||
li.appendChild(nameDiv);
|
||||
|
||||
// Progress bar container
|
||||
const progDiv = document.createElement("div");
|
||||
progDiv.classList.add("progress", "upload-progress-div");
|
||||
progDiv.style.flex = "0 0 250px";
|
||||
progDiv.style.marginLeft = "5px";
|
||||
const progBar = document.createElement("div");
|
||||
progBar.classList.add("progress-bar");
|
||||
progBar.style.width = "0%";
|
||||
progBar.innerText = "0%";
|
||||
progDiv.appendChild(progBar);
|
||||
li.appendChild(progDiv);
|
||||
|
||||
li.progressBar = progBar;
|
||||
li.startTime = Date.now();
|
||||
return li;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------
|
||||
Processing Files
|
||||
- For drag–and–drop, use original processing (supports folders).
|
||||
- For file picker, if using Resumable, those files use resumable.
|
||||
----------------------------------------------------- */
|
||||
function processFiles(filesInput) {
|
||||
const fileInfoContainer = document.getElementById("fileInfoContainer");
|
||||
const files = Array.from(filesInput);
|
||||
|
||||
if (fileInfoContainer) {
|
||||
if (files.length > 0) {
|
||||
if (files.length === 1) {
|
||||
fileInfoContainer.innerHTML = `
|
||||
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;">
|
||||
<span class="material-icons file-icon">insert_drive_file</span>
|
||||
</div>
|
||||
<span id="fileNameDisplay" class="file-name-display">${escapeHTML(files[0].name || files[0].fileName || "Unnamed File")}</span>
|
||||
`;
|
||||
} else {
|
||||
fileInfoContainer.innerHTML = `
|
||||
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;">
|
||||
<span class="material-icons file-icon">insert_drive_file</span>
|
||||
</div>
|
||||
<span id="fileCountDisplay" class="file-name-display">${files.length} files selected</span>
|
||||
`;
|
||||
}
|
||||
const previewContainer = document.getElementById("filePreviewContainer");
|
||||
if (previewContainer) {
|
||||
previewContainer.innerHTML = "";
|
||||
displayFilePreview(files[0], previewContainer);
|
||||
}
|
||||
} else {
|
||||
fileInfoContainer.innerHTML = `<span id="fileInfoDefault">No files selected</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
files.forEach((file, index) => {
|
||||
file.uploadIndex = index;
|
||||
});
|
||||
|
||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||
progressContainer.innerHTML = "";
|
||||
|
||||
if (files.length > 0) {
|
||||
const maxDisplay = 10;
|
||||
const list = document.createElement("ul");
|
||||
list.classList.add("upload-progress-list");
|
||||
|
||||
// Check for relative paths (for folder uploads).
|
||||
const hasRelativePaths = files.some(file => {
|
||||
const rel = file.webkitRelativePath || file.customRelativePath || "";
|
||||
return rel.trim() !== "";
|
||||
});
|
||||
|
||||
if (hasRelativePaths) {
|
||||
// Group files by folder.
|
||||
const fileGroups = {};
|
||||
files.forEach(file => {
|
||||
let folderName = "Root";
|
||||
const relativePath = file.webkitRelativePath || file.customRelativePath || "";
|
||||
if (relativePath.trim() !== "") {
|
||||
const parts = relativePath.split("/");
|
||||
if (parts.length > 1) {
|
||||
folderName = parts.slice(0, parts.length - 1).join("/");
|
||||
}
|
||||
}
|
||||
if (!fileGroups[folderName]) {
|
||||
fileGroups[folderName] = [];
|
||||
}
|
||||
fileGroups[folderName].push(file);
|
||||
});
|
||||
|
||||
Object.keys(fileGroups).forEach(folderName => {
|
||||
// Only show folder grouping if folderName is not "Root"
|
||||
if (folderName !== "Root") {
|
||||
const folderLi = document.createElement("li");
|
||||
folderLi.classList.add("upload-folder-group");
|
||||
folderLi.innerHTML = `<i class="material-icons folder-icon" style="vertical-align:middle; margin-right:8px;">folder</i> ${folderName}:`;
|
||||
list.appendChild(folderLi);
|
||||
}
|
||||
const nestedUl = document.createElement("ul");
|
||||
nestedUl.classList.add("upload-folder-group-list");
|
||||
fileGroups[folderName]
|
||||
.sort((a, b) => a.uploadIndex - b.uploadIndex)
|
||||
.forEach(file => {
|
||||
const li = createFileEntry(file);
|
||||
nestedUl.appendChild(li);
|
||||
});
|
||||
list.appendChild(nestedUl);
|
||||
});
|
||||
} else {
|
||||
// No relative paths – list files directly.
|
||||
files.forEach((file, index) => {
|
||||
const li = createFileEntry(file);
|
||||
li.style.display = (index < maxDisplay) ? "flex" : "none";
|
||||
li.dataset.uploadIndex = index;
|
||||
list.appendChild(li);
|
||||
});
|
||||
if (files.length > maxDisplay) {
|
||||
const extra = document.createElement("li");
|
||||
extra.classList.add("upload-progress-extra");
|
||||
extra.textContent = `Uploading additional ${files.length - maxDisplay} file(s)...`;
|
||||
extra.style.display = "flex";
|
||||
list.appendChild(extra);
|
||||
}
|
||||
}
|
||||
const listWrapper = document.createElement("div");
|
||||
listWrapper.classList.add("upload-progress-wrapper");
|
||||
listWrapper.style.maxHeight = "300px";
|
||||
listWrapper.style.overflowY = "auto";
|
||||
listWrapper.appendChild(list);
|
||||
progressContainer.appendChild(listWrapper);
|
||||
}
|
||||
|
||||
adjustFolderHelpExpansion();
|
||||
window.addEventListener("resize", adjustFolderHelpExpansion);
|
||||
|
||||
window.selectedFiles = files;
|
||||
updateFileInfoCount();
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------
|
||||
Resumable.js Integration for File Picker Uploads
|
||||
(Only files chosen via file input use Resumable; folder uploads use original code.)
|
||||
----------------------------------------------------- */
|
||||
const useResumable = true; // Enable resumable for file picker uploads
|
||||
let resumableInstance;
|
||||
function initResumableUpload() {
|
||||
resumableInstance = new Resumable({
|
||||
target: "upload.php",
|
||||
query: { folder: window.currentFolder || "root", upload_token: window.csrfToken },
|
||||
chunkSize: 1.5 * 1024 * 1024, // 1.5 MB chunks
|
||||
simultaneousUploads: 3,
|
||||
testChunks: false,
|
||||
throttleProgressCallbacks: 1,
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
});
|
||||
|
||||
const fileInput = document.getElementById("file");
|
||||
if (fileInput) {
|
||||
// Assign Resumable to file input for file picker uploads.
|
||||
resumableInstance.assignBrowse(fileInput);
|
||||
fileInput.addEventListener("change", function () {
|
||||
for (let i = 0; i < fileInput.files.length; i++) {
|
||||
resumableInstance.addFile(fileInput.files[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resumableInstance.on("fileAdded", function (file) {
|
||||
// Initialize custom paused flag
|
||||
file.paused = false;
|
||||
file.uploadIndex = file.uniqueIdentifier;
|
||||
if (!window.selectedFiles) {
|
||||
window.selectedFiles = [];
|
||||
}
|
||||
window.selectedFiles.push(file);
|
||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||
|
||||
// Check if a wrapper already exists; if not, create one with a UL inside.
|
||||
let listWrapper = progressContainer.querySelector(".upload-progress-wrapper");
|
||||
let list;
|
||||
if (!listWrapper) {
|
||||
listWrapper = document.createElement("div");
|
||||
listWrapper.classList.add("upload-progress-wrapper");
|
||||
listWrapper.style.maxHeight = "300px";
|
||||
listWrapper.style.overflowY = "auto";
|
||||
list = document.createElement("ul");
|
||||
list.classList.add("upload-progress-list");
|
||||
listWrapper.appendChild(list);
|
||||
progressContainer.appendChild(listWrapper);
|
||||
} else {
|
||||
list = listWrapper.querySelector("ul.upload-progress-list");
|
||||
}
|
||||
|
||||
const li = createFileEntry(file);
|
||||
li.dataset.uploadIndex = file.uniqueIdentifier;
|
||||
list.appendChild(li);
|
||||
updateFileInfoCount();
|
||||
});
|
||||
|
||||
resumableInstance.on("fileProgress", function(file) {
|
||||
const progress = file.progress(); // value between 0 and 1
|
||||
const percent = Math.floor(progress * 100);
|
||||
const li = document.querySelector(`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`);
|
||||
if (li && li.progressBar) {
|
||||
if (percent < 99) {
|
||||
li.progressBar.style.width = percent + "%";
|
||||
|
||||
// Calculate elapsed time and speed.
|
||||
const elapsed = (Date.now() - li.startTime) / 1000;
|
||||
let speed = "";
|
||||
if (elapsed > 0) {
|
||||
const bytesUploaded = progress * file.size;
|
||||
const spd = bytesUploaded / elapsed;
|
||||
if (spd < 1024) {
|
||||
speed = spd.toFixed(0) + " B/s";
|
||||
} else if (spd < 1048576) {
|
||||
speed = (spd / 1024).toFixed(1) + " KB/s";
|
||||
} else {
|
||||
speed = (spd / 1048576).toFixed(1) + " MB/s";
|
||||
}
|
||||
}
|
||||
li.progressBar.innerText = percent + "% (" + speed + ")";
|
||||
} else {
|
||||
// When progress reaches 99% or higher, show only a spinner icon.
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerHTML = '<i class="material-icons spinning" style="vertical-align: middle;">autorenew</i>';
|
||||
}
|
||||
|
||||
// Enable the pause/resume button once progress starts.
|
||||
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
||||
if (pauseResumeBtn) {
|
||||
pauseResumeBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resumableInstance.on("fileSuccess", function(file, message) {
|
||||
const li = document.querySelector(`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`);
|
||||
if (li && li.progressBar) {
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerText = "Done";
|
||||
// Hide pause/resume and remove buttons for successful files.
|
||||
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
||||
if (pauseResumeBtn) {
|
||||
pauseResumeBtn.style.display = "none";
|
||||
}
|
||||
const removeBtn = li.querySelector(".remove-file-btn");
|
||||
if (removeBtn) {
|
||||
removeBtn.style.display = "none";
|
||||
}
|
||||
// Schedule removal of the file entry after 5 seconds.
|
||||
setTimeout(() => {
|
||||
li.remove();
|
||||
window.selectedFiles = window.selectedFiles.filter(f => f.uniqueIdentifier !== file.uniqueIdentifier);
|
||||
updateFileInfoCount();
|
||||
}, 5000);
|
||||
}
|
||||
loadFileList(window.currentFolder);
|
||||
});
|
||||
|
||||
|
||||
|
||||
resumableInstance.on("fileError", function (file, message) {
|
||||
const li = document.querySelector(`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`);
|
||||
if (li && li.progressBar) {
|
||||
li.progressBar.innerText = "Error";
|
||||
}
|
||||
// Mark file as errored so that the pause/resume button acts as a restart button.
|
||||
file.isError = true;
|
||||
const pauseResumeBtn = li ? li.querySelector(".pause-resume-btn") : null;
|
||||
if (pauseResumeBtn) {
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">replay</span>';
|
||||
pauseResumeBtn.disabled = false;
|
||||
}
|
||||
showToast("Error uploading file: " + file.fileName);
|
||||
});
|
||||
|
||||
resumableInstance.on("complete", function () {
|
||||
// If any file is marked with an error, leave the list intact.
|
||||
const hasError = window.selectedFiles.some(f => f.isError);
|
||||
if (!hasError) {
|
||||
// All files succeeded—clear the file input and progress container after 5 seconds.
|
||||
setTimeout(() => {
|
||||
const fileInput = document.getElementById("file");
|
||||
if (fileInput) fileInput.value = "";
|
||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||
progressContainer.innerHTML = "";
|
||||
window.selectedFiles = [];
|
||||
adjustFolderHelpExpansionClosed();
|
||||
const fileInfoContainer = document.getElementById("fileInfoContainer");
|
||||
if (fileInfoContainer) {
|
||||
fileInfoContainer.innerHTML = `<span id="fileInfoDefault">No files selected</span>`;
|
||||
}
|
||||
const dropArea = document.getElementById("uploadDropArea");
|
||||
if (dropArea) setDropAreaDefault();
|
||||
}, 5000);
|
||||
} else {
|
||||
showToast("Some files failed to upload. Please check the list.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------
|
||||
XHR-based submitFiles for Drag–and–Drop (Folder) Uploads
|
||||
----------------------------------------------------- */
|
||||
function submitFiles(allFiles) {
|
||||
const folderToUse = window.currentFolder || "root";
|
||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||
const fileInput = document.getElementById("file");
|
||||
|
||||
const progressElements = {};
|
||||
const listItems = progressContainer.querySelectorAll("li.upload-progress-item");
|
||||
listItems.forEach(item => {
|
||||
progressElements[item.dataset.uploadIndex] = item;
|
||||
});
|
||||
|
||||
let finishedCount = 0;
|
||||
let allSucceeded = true;
|
||||
const uploadResults = new Array(allFiles.length).fill(false);
|
||||
|
||||
allFiles.forEach(file => {
|
||||
const formData = new FormData();
|
||||
formData.append("file[]", file);
|
||||
formData.append("folder", folderToUse);
|
||||
// Append CSRF token as "upload_token"
|
||||
formData.append("upload_token", window.csrfToken);
|
||||
const relativePath = file.webkitRelativePath || file.customRelativePath || "";
|
||||
if (relativePath.trim() !== "") {
|
||||
formData.append("relativePath", relativePath);
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
let currentPercent = 0;
|
||||
|
||||
xhr.upload.addEventListener("progress", function (e) {
|
||||
if (e.lengthComputable) {
|
||||
currentPercent = Math.round((e.loaded / e.total) * 100);
|
||||
const li = progressElements[file.uploadIndex];
|
||||
if (li) {
|
||||
const elapsed = (Date.now() - li.startTime) / 1000;
|
||||
let speed = "";
|
||||
if (elapsed > 0) {
|
||||
const spd = e.loaded / elapsed;
|
||||
if (spd < 1024) speed = spd.toFixed(0) + " B/s";
|
||||
else if (spd < 1048576) speed = (spd / 1024).toFixed(1) + " KB/s";
|
||||
else speed = (spd / 1048576).toFixed(1) + " MB/s";
|
||||
}
|
||||
li.progressBar.style.width = currentPercent + "%";
|
||||
li.progressBar.innerText = currentPercent + "% (" + speed + ")";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("load", function () {
|
||||
let jsonResponse;
|
||||
try {
|
||||
jsonResponse = JSON.parse(xhr.responseText);
|
||||
} catch (e) {
|
||||
jsonResponse = null;
|
||||
}
|
||||
const li = progressElements[file.uploadIndex];
|
||||
if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) {
|
||||
if (li) {
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerText = "Done";
|
||||
if (li.removeBtn) li.removeBtn.style.display = "none";
|
||||
}
|
||||
uploadResults[file.uploadIndex] = true;
|
||||
} else {
|
||||
if (li) {
|
||||
li.progressBar.innerText = "Error";
|
||||
}
|
||||
allSucceeded = false;
|
||||
}
|
||||
finishedCount++;
|
||||
if (finishedCount === allFiles.length) {
|
||||
refreshFileList(allFiles, uploadResults, progressElements);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("error", function () {
|
||||
const li = progressElements[file.uploadIndex];
|
||||
if (li) {
|
||||
li.progressBar.innerText = "Error";
|
||||
}
|
||||
uploadResults[file.uploadIndex] = false;
|
||||
allSucceeded = false;
|
||||
finishedCount++;
|
||||
if (finishedCount === allFiles.length) {
|
||||
refreshFileList(allFiles, uploadResults, progressElements);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("abort", function () {
|
||||
const li = progressElements[file.uploadIndex];
|
||||
if (li) {
|
||||
li.progressBar.innerText = "Aborted";
|
||||
}
|
||||
uploadResults[file.uploadIndex] = false;
|
||||
allSucceeded = false;
|
||||
finishedCount++;
|
||||
if (finishedCount === allFiles.length) {
|
||||
refreshFileList(allFiles, uploadResults, progressElements);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.open("POST", "upload.php", true);
|
||||
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||
xhr.send(formData);
|
||||
});
|
||||
|
||||
function refreshFileList(allFiles, uploadResults, progressElements) {
|
||||
loadFileList(folderToUse)
|
||||
.then(serverFiles => {
|
||||
initFileActions();
|
||||
serverFiles = (serverFiles || []).map(item => item.name.trim().toLowerCase());
|
||||
let overallSuccess = true;
|
||||
allFiles.forEach(file => {
|
||||
const clientFileName = file.name.trim().toLowerCase();
|
||||
const li = progressElements[file.uploadIndex];
|
||||
if (!uploadResults[file.uploadIndex] || !serverFiles.includes(clientFileName)) {
|
||||
if (li) {
|
||||
li.progressBar.innerText = "Error";
|
||||
}
|
||||
overallSuccess = false;
|
||||
} else if (li) {
|
||||
// Schedule removal of successful file entry after 5 seconds.
|
||||
setTimeout(() => {
|
||||
li.remove();
|
||||
delete progressElements[file.uploadIndex];
|
||||
updateFileInfoCount();
|
||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||
if (progressContainer && progressContainer.querySelectorAll("li.upload-progress-item").length === 0) {
|
||||
const fileInput = document.getElementById("file");
|
||||
if (fileInput) fileInput.value = "";
|
||||
progressContainer.innerHTML = "";
|
||||
adjustFolderHelpExpansionClosed();
|
||||
const fileInfoContainer = document.getElementById("fileInfoContainer");
|
||||
if (fileInfoContainer) {
|
||||
fileInfoContainer.innerHTML = `<span id="fileInfoDefault">No files selected</span>`;
|
||||
}
|
||||
const dropArea = document.getElementById("uploadDropArea");
|
||||
if (dropArea) setDropAreaDefault();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
if (!overallSuccess) {
|
||||
showToast("Some files failed to upload. Please check the list.");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching file list:", error);
|
||||
showToast("Some files may have failed to upload. Please check the list.");
|
||||
})
|
||||
.finally(() => {
|
||||
loadFolderTree(window.currentFolder);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------
|
||||
Main initUpload: Sets up file input, drop area, and form submission.
|
||||
----------------------------------------------------- */
|
||||
function initUpload() {
|
||||
const fileInput = document.getElementById("file");
|
||||
const dropArea = document.getElementById("uploadDropArea");
|
||||
const uploadForm = document.getElementById("uploadFileForm");
|
||||
|
||||
// For file picker, remove directory attributes so only files can be chosen.
|
||||
if (fileInput) {
|
||||
fileInput.removeAttribute("webkitdirectory");
|
||||
fileInput.removeAttribute("mozdirectory");
|
||||
fileInput.removeAttribute("directory");
|
||||
fileInput.setAttribute("multiple", "");
|
||||
}
|
||||
|
||||
setDropAreaDefault();
|
||||
|
||||
// Drag–and–drop events (for folder uploads) use original processing.
|
||||
if (dropArea) {
|
||||
dropArea.classList.add("upload-drop-area");
|
||||
dropArea.addEventListener("dragover", function (e) {
|
||||
e.preventDefault();
|
||||
dropArea.style.backgroundColor = document.body.classList.contains("dark-mode") ? "#333" : "#f8f8f8";
|
||||
});
|
||||
dropArea.addEventListener("dragleave", function (e) {
|
||||
e.preventDefault();
|
||||
dropArea.style.backgroundColor = "";
|
||||
});
|
||||
dropArea.addEventListener("drop", function (e) {
|
||||
e.preventDefault();
|
||||
dropArea.style.backgroundColor = "";
|
||||
const dt = e.dataTransfer;
|
||||
if (dt.items && dt.items.length > 0) {
|
||||
getFilesFromDataTransferItems(dt.items).then(files => {
|
||||
if (files.length > 0) {
|
||||
processFiles(files);
|
||||
}
|
||||
});
|
||||
} else if (dt.files && dt.files.length > 0) {
|
||||
processFiles(dt.files);
|
||||
}
|
||||
});
|
||||
// Clicking drop area triggers file input.
|
||||
dropArea.addEventListener("click", function () {
|
||||
if (fileInput) fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener("change", function () {
|
||||
if (useResumable) {
|
||||
// For file picker, if resumable is enabled, let it handle the files.
|
||||
for (let i = 0; i < fileInput.files.length; i++) {
|
||||
resumableInstance.addFile(fileInput.files[i]);
|
||||
}
|
||||
} else {
|
||||
processFiles(fileInput.files);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadForm) {
|
||||
uploadForm.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
const files = window.selectedFiles || (fileInput ? fileInput.files : []);
|
||||
if (!files || files.length === 0) {
|
||||
showToast("No files selected.");
|
||||
return;
|
||||
}
|
||||
// If files come from file picker (no relative path), use Resumable.
|
||||
if (useResumable && (!files[0].customRelativePath || files[0].customRelativePath === "")) {
|
||||
// Ensure current folder is updated.
|
||||
resumableInstance.opts.query.folder = window.currentFolder || "root";
|
||||
resumableInstance.upload();
|
||||
showToast("Resumable upload started...");
|
||||
} else {
|
||||
submitFiles(files);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (useResumable) {
|
||||
initResumableUpload();
|
||||
}
|
||||
}
|
||||
|
||||
export { initUpload };
|
||||
Reference in New Issue
Block a user