Fuse.js Integration for Indexed Real-Time Searching & Dependencies added
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Changes 4/12/2025
|
||||||
|
|
||||||
|
- **Fuse.js Integration for Indexed Real-Time Searching**
|
||||||
|
- **Added Fuse.js Library:** Included Fuse.js via a CDN `<script>` tag to leverage its client‑side fuzzy search capabilities.
|
||||||
|
- **Created searchFiles Helper Function:** Introduced a new function that uses Fuse.js to build an index and perform fuzzy searches over file properties (file name, uploader, and nested tag names).
|
||||||
|
- **Transformed JSON Object to Array:** Updated the loadFileList() function to convert the returned file data into an array (if it isn’t already) and assign file names from JSON keys.
|
||||||
|
- **Updated Rendering Functions:** Modified both renderFileTable() and renderGalleryView() to use the searchFiles() helper instead of a simple in‑array .filter(). This ensures that every search—real‑time by user input—is powered by Fuse.js’s indexed search.
|
||||||
|
- **Enhanced Search Configuration:** Configured Fuse.js to search across multiple keys (file name, uploader, and tags) so that users can find files based on any of these properties.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 4/11/2025
|
## Changes 4/11/2025
|
||||||
|
|
||||||
- Fixed fileDragDrop issue from previous update.
|
- Fixed fileDragDrop issue from previous update.
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -177,6 +177,26 @@ Areas where you can help: translations, bug fixes, UI improvements, or building
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### PHP Libraries
|
||||||
|
|
||||||
|
- **[jumbojett/openid-connect-php](https://github.com/jumbojett/OpenID-Connect-PHP)** (v^1.0.0)
|
||||||
|
- **[phpseclib/phpseclib](https://github.com/phpseclib/phpseclib)** (v~3.0.7)
|
||||||
|
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.0)
|
||||||
|
- **[endroid/qr-code](https://github.com/endroid/qr-code)** (v^5.0)
|
||||||
|
|
||||||
|
### Client-Side Libraries
|
||||||
|
|
||||||
|
- **Google Fonts** – [Roboto](https://fonts.google.com/specimen/Roboto) and **Material Icons** ([Google Material Icons](https://fonts.google.com/icons))
|
||||||
|
- **[Bootstrap](https://getbootstrap.com/)** (v4.5.2)
|
||||||
|
- **[CodeMirror](https://codemirror.net/)** (v5.65.5) – For code editing functionality.
|
||||||
|
- **[Resumable.js](http://www.resumablejs.com/)** (v1.1.0) – For file uploads.
|
||||||
|
- **[DOMPurify](https://github.com/cure53/DOMPurify)** (v2.4.0) – For sanitizing HTML.
|
||||||
|
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) – For indexed, fuzzy searching.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is open-source under the MIT License. That means you’re free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!
|
This project is open-source under the MIT License. That means you’re free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!
|
||||||
|
|||||||
@@ -41,6 +41,9 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
||||||
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
|
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"
|
||||||
|
integrity="sha384-zPE55eyESN+FxCWGEnlNxGyAPJud6IZ6TtJmXb56OFRGhxZPN4akj9rjA3gw5Qqa"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
<link rel="stylesheet" href="css/styles.css" />
|
<link rel="stylesheet" href="css/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,8 @@ window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "ga
|
|||||||
*/
|
*/
|
||||||
function parseSizeToBytes(sizeStr) {
|
function parseSizeToBytes(sizeStr) {
|
||||||
if (!sizeStr) return 0;
|
if (!sizeStr) return 0;
|
||||||
// Remove any whitespace
|
|
||||||
let s = sizeStr.trim();
|
let s = sizeStr.trim();
|
||||||
// Extract the numerical part.
|
|
||||||
let value = parseFloat(s);
|
let value = parseFloat(s);
|
||||||
// Determine if there is a unit. Convert the unit to uppercase for easier matching.
|
|
||||||
let upper = s.toUpperCase();
|
let upper = s.toUpperCase();
|
||||||
if (upper.includes("KB")) {
|
if (upper.includes("KB")) {
|
||||||
value *= 1024;
|
value *= 1024;
|
||||||
@@ -50,7 +47,7 @@ function parseSizeToBytes(sizeStr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format the total bytes as a human-readable string, choosing an appropriate unit.
|
* Format the total bytes as a human-readable string.
|
||||||
*/
|
*/
|
||||||
function formatSize(totalBytes) {
|
function formatSize(totalBytes) {
|
||||||
if (totalBytes < 1024) {
|
if (totalBytes < 1024) {
|
||||||
@@ -66,18 +63,33 @@ function formatSize(totalBytes) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the folder summary HTML using the filtered file list.
|
* Build the folder summary HTML using the filtered file list.
|
||||||
* This function sums the file sizes in bytes correctly, then formats the total.
|
|
||||||
*/
|
*/
|
||||||
function buildFolderSummary(filteredFiles) {
|
function buildFolderSummary(filteredFiles) {
|
||||||
const totalFiles = filteredFiles.length;
|
const totalFiles = filteredFiles.length;
|
||||||
const totalBytes = filteredFiles.reduce((sum, file) => {
|
const totalBytes = filteredFiles.reduce((sum, file) => {
|
||||||
// file.size might be something like "456.9KB" or just "1024".
|
|
||||||
return sum + parseSizeToBytes(file.size);
|
return sum + parseSizeToBytes(file.size);
|
||||||
}, 0);
|
}, 0);
|
||||||
const sizeStr = formatSize(totalBytes);
|
const sizeStr = formatSize(totalBytes);
|
||||||
return `<strong>Total Files:</strong> ${totalFiles} | <strong>Total Size:</strong> ${sizeStr}`;
|
return `<strong>Total Files:</strong> ${totalFiles} | <strong>Total Size:</strong> ${sizeStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --- Fuse.js Search Helper ---
|
||||||
|
* Uses Fuse.js to perform a fuzzy search on fileData.
|
||||||
|
* Searches over file name, uploader, and tag names.
|
||||||
|
*/
|
||||||
|
function searchFiles(searchTerm) {
|
||||||
|
if (!searchTerm) return fileData;
|
||||||
|
// Define search options – adjust threshold as needed.
|
||||||
|
const options = {
|
||||||
|
keys: ['name', 'uploader', 'tags.name'],
|
||||||
|
threshold: 0.3
|
||||||
|
};
|
||||||
|
const fuse = new Fuse(fileData, options);
|
||||||
|
// Fuse returns an array of results where each result has an "item" property.
|
||||||
|
return fuse.search(searchTerm).map(result => result.item);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --- VIEW MODE TOGGLE BUTTON & Helpers ---
|
* --- VIEW MODE TOGGLE BUTTON & Helpers ---
|
||||||
*/
|
*/
|
||||||
@@ -134,7 +146,15 @@ export function loadFileList(folderParam) {
|
|||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
fileListContainer.innerHTML = ""; // Clear loading message.
|
fileListContainer.innerHTML = ""; // Clear loading message.
|
||||||
if (data.files && data.files.length > 0) {
|
if (data.files && Object.keys(data.files).length > 0) {
|
||||||
|
// In case the returned "files" is an object instead of an array, transform it.
|
||||||
|
if (!Array.isArray(data.files)) {
|
||||||
|
data.files = Object.entries(data.files).map(([name, meta]) => {
|
||||||
|
meta.name = name;
|
||||||
|
return meta;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Process each file – add computed properties.
|
||||||
data.files = data.files.map(file => {
|
data.files = data.files.map(file => {
|
||||||
file.fullName = (file.path || file.name).trim().toLowerCase();
|
file.fullName = (file.path || file.name).trim().toLowerCase();
|
||||||
file.editable = canEditFile(file.name);
|
file.editable = canEditFile(file.name);
|
||||||
@@ -146,7 +166,7 @@ export function loadFileList(folderParam) {
|
|||||||
});
|
});
|
||||||
fileData = data.files;
|
fileData = data.files;
|
||||||
|
|
||||||
// Update the file list actions area without removing existing buttons.
|
// Update file summary.
|
||||||
const actionsContainer = document.getElementById("fileListActions");
|
const actionsContainer = document.getElementById("fileListActions");
|
||||||
if (actionsContainer) {
|
if (actionsContainer) {
|
||||||
let summaryElem = document.getElementById("fileSummary");
|
let summaryElem = document.getElementById("fileSummary");
|
||||||
@@ -164,7 +184,7 @@ export function loadFileList(folderParam) {
|
|||||||
summaryElem.innerHTML = buildFolderSummary(fileData);
|
summaryElem.innerHTML = buildFolderSummary(fileData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the view normally.
|
// Render view based on the view mode.
|
||||||
if (window.viewMode === "gallery") {
|
if (window.viewMode === "gallery") {
|
||||||
renderGalleryView(folder);
|
renderGalleryView(folder);
|
||||||
} else {
|
} else {
|
||||||
@@ -193,8 +213,7 @@ export function loadFileList(folderParam) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update renderFileTable so that it writes its content into the provided container.
|
* Update renderFileTable so it writes its content into the provided container.
|
||||||
* If no container is provided, it defaults to the element with id "fileList".
|
|
||||||
*/
|
*/
|
||||||
export function renderFileTable(folder, container) {
|
export function renderFileTable(folder, container) {
|
||||||
const fileListContent = container || document.getElementById("fileList");
|
const fileListContent = container || document.getElementById("fileList");
|
||||||
@@ -202,11 +221,9 @@ export function renderFileTable(folder, container) {
|
|||||||
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
||||||
let currentPage = window.currentPage || 1;
|
let currentPage = window.currentPage || 1;
|
||||||
|
|
||||||
const filteredFiles = fileData.filter(file => {
|
// Use Fuse.js search via our helper function.
|
||||||
const nameMatch = file.name.toLowerCase().includes(searchTerm);
|
const filteredFiles = searchFiles(searchTerm);
|
||||||
const tagMatch = file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
|
|
||||||
return nameMatch || tagMatch;
|
|
||||||
});
|
|
||||||
const totalFiles = filteredFiles.length;
|
const totalFiles = filteredFiles.length;
|
||||||
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting);
|
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting);
|
||||||
if (currentPage > totalPages) {
|
if (currentPage > totalPages) {
|
||||||
@@ -257,7 +274,7 @@ export function renderFileTable(folder, container) {
|
|||||||
|
|
||||||
createViewToggleButton();
|
createViewToggleButton();
|
||||||
|
|
||||||
// Setup event listeners as before...
|
// Setup event listeners.
|
||||||
const newSearchInput = document.getElementById("searchInput");
|
const newSearchInput = document.getElementById("searchInput");
|
||||||
if (newSearchInput) {
|
if (newSearchInput) {
|
||||||
newSearchInput.addEventListener("input", debounce(function () {
|
newSearchInput.addEventListener("input", debounce(function () {
|
||||||
@@ -317,10 +334,8 @@ export function renderFileTable(folder, container) {
|
|||||||
export function renderGalleryView(folder, container) {
|
export function renderGalleryView(folder, container) {
|
||||||
const fileListContent = container || document.getElementById("fileList");
|
const fileListContent = container || document.getElementById("fileList");
|
||||||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||||||
const filteredFiles = fileData.filter(file => {
|
// Use Fuse.js search for gallery view as well.
|
||||||
return file.name.toLowerCase().includes(searchTerm) ||
|
const filteredFiles = searchFiles(searchTerm);
|
||||||
(file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm)));
|
|
||||||
});
|
|
||||||
const folderPath = folder === "root"
|
const folderPath = folder === "root"
|
||||||
? "uploads/"
|
? "uploads/"
|
||||||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const translations = {
|
|||||||
"no_files_selected": "No files selected.",
|
"no_files_selected": "No files selected.",
|
||||||
"confirm_delete_files": "Are you sure you want to delete {count} selected file(s)?",
|
"confirm_delete_files": "Are you sure you want to delete {count} selected file(s)?",
|
||||||
"element_not_found": "Element with id \"{id}\" not found.",
|
"element_not_found": "Element with id \"{id}\" not found.",
|
||||||
"search_placeholder": "Search files or tag...",
|
"search_placeholder": "Search files, tags, or uploader...",
|
||||||
"file_name": "File Name",
|
"file_name": "File Name",
|
||||||
"date_modified": "Date Modified",
|
"date_modified": "Date Modified",
|
||||||
"upload_date": "Upload Date",
|
"upload_date": "Upload Date",
|
||||||
|
|||||||
Reference in New Issue
Block a user