Gallery View add selection actions and search filtering

This commit is contained in:
Ryan
2025-04-18 02:58:30 -04:00
committed by GitHub
parent 7e50ba1f70
commit e390a35e8a
3 changed files with 215 additions and 70 deletions

View File

@@ -1,5 +1,25 @@
# Changelog # Changelog
## Changes 4/18/2025
### fileListView.js
- Seed and persist `itemsPerPage` from `localStorage`
- Use `window.itemsPerPage` for pagination in gallery
- Enable search input filtering in gallery mode
- Always rerender the viewtoggle button on gallery load
- Restore percard action buttons (download, edit, rename, share)
- Assign real `value` to checkboxes and call `updateFileActionButtons()` on change
- Update `changePage` and `changeItemsPerPage` to respect `viewMode`
### fileTags.js
- Import `renderFileTable` and `renderGalleryView`
- Rerender the list after saving a singlefile tag
- Rerender the list after saving multifile tags
---
## Changes 4/17/2025 ## Changes 4/17/2025
- Generate OpenAPI spec and API HTML docs - Generate OpenAPI spec and API HTML docs

View File

@@ -20,9 +20,12 @@ import { openTagModal, openMultiTagModal } from './fileTags.js';
export let fileData = []; export let fileData = [];
export let sortOrder = { column: "uploaded", ascending: true }; export let sortOrder = { column: "uploaded", ascending: true };
window.itemsPerPage = window.itemsPerPage || 10; window.itemsPerPage = parseInt(
localStorage.getItem('itemsPerPage') || window.itemsPerPage || '10',
10
);
window.currentPage = window.currentPage || 1; window.currentPage = window.currentPage || 1;
window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery" window.viewMode = localStorage.getItem("viewMode") || "table";
// Global flag for advanced search mode. // Global flag for advanced search mode.
window.advancedSearchEnabled = false; window.advancedSearchEnabled = false;
@@ -407,33 +410,89 @@ export function renderGalleryView(folder, container) {
? "uploads/" ? "uploads/"
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
// Use the current global column value (default to 3). // pagination settings
const numColumns = window.galleryColumns || 3; const itemsPerPage = window.itemsPerPage;
let currentPage = window.currentPage || 1;
const totalFiles = filteredFiles.length;
const totalPages = Math.ceil(totalFiles / itemsPerPage);
if (currentPage > totalPages) {
currentPage = totalPages || 1;
window.currentPage = currentPage;
}
// --- Insert slider controls --- // --- Top controls: search + pagination + items-per-page ---
const sliderHTML = ` let galleryHTML = buildSearchAndPaginationControls({
<div class="gallery-slider" style="margin: 10px; text-align: center;"> currentPage,
<label for="galleryColumnsSlider" style="margin-right: 5px;">${t('columns')}:</label> totalPages,
<input type="range" id="galleryColumnsSlider" min="1" max="6" value="${numColumns}" style="vertical-align: middle;"> searchTerm: window.currentSearchTerm || ""
});
// wire up search input just like table view
setTimeout(() => {
const searchInput = document.getElementById("searchInput");
if (searchInput) {
searchInput.addEventListener("input", debounce(() => {
window.currentSearchTerm = searchInput.value;
window.currentPage = 1;
renderGalleryView(folder);
// keep caret at end
setTimeout(() => {
const f = document.getElementById("searchInput");
if (f) {
f.focus();
const len = f.value.length;
f.setSelectionRange(len, len);
}
}, 0);
}, 300));
}
}, 0);
// --- Column slider ---
const numColumns = window.galleryColumns || 3;
galleryHTML += `
<div class="gallery-slider" style="margin:10px; text-align:center;">
<label for="galleryColumnsSlider" style="margin-right:5px;">
${t('columns')}:
</label>
<input type="range" id="galleryColumnsSlider" min="1" max="6"
value="${numColumns}" style="vertical-align:middle;">
<span id="galleryColumnsValue">${numColumns}</span> <span id="galleryColumnsValue">${numColumns}</span>
</div> </div>
`; `;
// Set up the grid container using the slider's current value. // --- Start gallery grid ---
const gridStyle = `display: grid; grid-template-columns: repeat(${numColumns}, 1fr); gap: 10px; padding: 10px;`; galleryHTML += `
<div class="gallery-container"
style="display:grid;
grid-template-columns:repeat(${numColumns},1fr);
gap:10px;
padding:10px;">
`;
// Build the gallery container HTML including the slider. // slice current page
let galleryHTML = sliderHTML; const startIdx = (currentPage - 1) * itemsPerPage;
galleryHTML += `<div class="gallery-container" style="${gridStyle}">`; const pageFiles = filteredFiles.slice(startIdx, startIdx + itemsPerPage);
filteredFiles.forEach((file) => {
pageFiles.forEach((file, idx) => {
const idSafe = encodeURIComponent(file.name) + "-" + (startIdx + idx);
// thumbnail
let thumbnail; let thumbnail;
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) { if (/\.(jpe?g|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
const cacheKey = folderPath + encodeURIComponent(file.name); const cacheKey = folderPath + encodeURIComponent(file.name);
if (window.imageCache && window.imageCache[cacheKey]) { if (window.imageCache && window.imageCache[cacheKey]) {
thumbnail = `<img src="${window.imageCache[cacheKey]}" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: ${getMaxImageHeight()}px; display: block; margin: 0 auto;">`; thumbnail = `<img src="${window.imageCache[cacheKey]}"
class="gallery-thumbnail"
alt="${escapeHTML(file.name)}"
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
} else { } else {
const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + new Date().getTime(); const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + Date.now();
thumbnail = `<img src="${imageUrl}" onload="cacheImage(this, '${cacheKey}')" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: ${getMaxImageHeight()}px; display: block; margin: 0 auto;">`; thumbnail = `<img src="${imageUrl}"
onload="cacheImage(this,'${cacheKey}')"
class="gallery-thumbnail"
alt="${escapeHTML(file.name)}"
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
} }
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) { } else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`; thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
@@ -441,82 +500,127 @@ export function renderGalleryView(folder, container) {
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`; thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
} }
// tag badges
let tagBadgesHTML = ""; let tagBadgesHTML = "";
if (file.tags && file.tags.length > 0) { if (file.tags && file.tags.length) {
tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`; tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`;
file.tags.forEach(tag => { file.tags.forEach(tag => {
tagBadgesHTML += `<span style="background-color: ${tag.color}; color: #fff; padding: 2px 4px; border-radius: 3px; margin-right: 2px; font-size: 0.8em;">${escapeHTML(tag.name)}</span>`; tagBadgesHTML += `<span style="background-color:${tag.color};
color:#fff;
padding:2px 4px;
border-radius:3px;
margin-right:2px;
font-size:0.8em;">
${escapeHTML(tag.name)}
</span>`;
}); });
tagBadgesHTML += `</div>`; tagBadgesHTML += `</div>`;
} }
// card with checkbox, preview, info, buttons
galleryHTML += ` galleryHTML += `
<div class="gallery-card" style="border: 1px solid #ccc; padding: 5px; text-align: center;"> <div class="gallery-card"
<div class="gallery-preview" style="cursor: pointer;" onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t=' + new Date().getTime(), '${file.name}')"> style="position:relative; border:1px solid #ccc; padding:5px; text-align:center;">
<input type="checkbox"
class="file-checkbox"
id="cb-${idSafe}"
value="${escapeHTML(file.name)}"
style="position:absolute; top:5px; left:5px; z-index:10;">
<label for="cb-${idSafe}"
style="position:absolute; top:5px; left:5px; width:16px; height:16px;"></label>
<div class="gallery-preview"
style="cursor:pointer;"
onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t='+Date.now(), '${file.name}')">
${thumbnail} ${thumbnail}
</div> </div>
<div class="gallery-info" style="margin-top: 5px;">
<span class="gallery-file-name" style="display: block; white-space: normal; overflow-wrap: break-word; word-wrap: break-word;">${escapeHTML(file.name)}</span> <div class="gallery-info" style="margin-top:5px;">
<span class="gallery-file-name"
style="display:block; white-space:normal; overflow-wrap:break-word;">
${escapeHTML(file.name)}
</span>
${tagBadgesHTML} ${tagBadgesHTML}
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
<button type="button" class="btn btn-sm btn-success download-btn" <div class="button-wrap" style="display:flex; justify-content:center; gap:5px; margin-top:5px;">
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')" <button type="button" class="btn btn-sm btn-success download-btn"
title="${t('download')}"> onclick="openDownloadModal('${file.name}', '${file.folder || "root"}')"
<i class="material-icons">file_download</i> title="${t('download')}">
<i class="material-icons">file_download</i>
</button> </button>
${file.editable ? ` ${file.editable ? `
<button class="btn btn-sm edit-btn" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="${t('Edit')}"> <button class="btn btn-sm edit-btn"
<i class="material-icons">edit</i> onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
</button> title="${t('Edit')}">
` : ""} <i class="material-icons">edit</i>
<button class="btn btn-sm btn-warning rename-btn" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="${t('rename')}"> </button>` : ""}
<i class="material-icons">drive_file_rename_outline</i> <button class="btn btn-sm btn-warning rename-btn"
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
title="${t('rename')}">
<i class="material-icons">drive_file_rename_outline</i>
</button> </button>
<button class="btn btn-sm btn-secondary share-btn" data-file="${escapeHTML(file.name)}" title="${t('share')}"> <button class="btn btn-sm btn-secondary share-btn"
<i class="material-icons">share</i> data-file="${escapeHTML(file.name)}"
title="${t('share')}">
<i class="material-icons">share</i>
</button> </button>
</div> </div>
</div> </div>
</div>`; </div>
`;
}); });
galleryHTML += "</div>"; // End gallery container.
galleryHTML += `</div>`; // end gallery-container
// bottom controls
galleryHTML += buildBottomControls(itemsPerPage);
// render
fileListContent.innerHTML = galleryHTML; fileListContent.innerHTML = galleryHTML;
// Re-apply slider constraints for the newly rendered slider. // ensure toggle button
updateSliderConstraints();
createViewToggleButton(); createViewToggleButton();
// Attach share button event listeners.
document.querySelectorAll(".share-btn").forEach(btn => { // attach listeners
btn.addEventListener("click", e => {
e.stopPropagation(); // checkboxes
const fileName = btn.getAttribute("data-file"); document.querySelectorAll(".file-checkbox").forEach(cb => {
const file = fileData.find(f => f.name === fileName); cb.addEventListener("change", () => updateFileActionButtons());
if (file) {
import('./filePreview.js').then(module => {
module.openShareModal(file, folder);
});
}
});
}); });
// --- Slider Event Listener --- // slider
const slider = document.getElementById("galleryColumnsSlider"); const slider = document.getElementById("galleryColumnsSlider");
if (slider) { if (slider) {
slider.addEventListener("input", function () { slider.addEventListener("input", () => {
const value = this.value; const v = +slider.value;
document.getElementById("galleryColumnsValue").textContent = value; document.getElementById("galleryColumnsValue").textContent = v;
window.galleryColumns = value; window.galleryColumns = v;
const galleryContainer = document.querySelector(".gallery-container"); document.querySelector(".gallery-container")
if (galleryContainer) { .style.gridTemplateColumns = `repeat(${v},1fr)`;
galleryContainer.style.gridTemplateColumns = `repeat(${value}, 1fr)`; document.querySelectorAll(".gallery-thumbnail")
} .forEach(img => img.style.maxHeight = getMaxImageHeight() + "px");
const newMaxHeight = getMaxImageHeight();
document.querySelectorAll(".gallery-thumbnail").forEach(img => {
img.style.maxHeight = newMaxHeight + "px";
});
}); });
} }
// pagination
window.changePage = newPage => {
window.currentPage = newPage;
if (window.viewMode === "gallery") renderGalleryView(folder);
else renderFileTable(folder);
};
// items per page
window.changeItemsPerPage = cnt => {
window.itemsPerPage = +cnt;
localStorage.setItem("itemsPerPage", cnt);
window.currentPage = 1;
if (window.viewMode === "gallery") renderGalleryView(folder);
else renderFileTable(folder);
};
// update toolbar buttons
updateFileActionButtons();
} }
// Responsive slider constraints based on screen size. // Responsive slider constraints based on screen size.
@@ -638,12 +742,22 @@ export function canEditFile(fileName) {
// Expose global functions for pagination and preview. // Expose global functions for pagination and preview.
window.changePage = function (newPage) { window.changePage = function (newPage) {
window.currentPage = newPage; window.currentPage = newPage;
renderFileTable(window.currentFolder); if (window.viewMode === 'gallery') {
renderGalleryView(window.currentFolder);
} else {
renderFileTable(window.currentFolder);
}
}; };
window.changeItemsPerPage = function (newCount) { window.changeItemsPerPage = function (newCount) {
window.itemsPerPage = parseInt(newCount); window.itemsPerPage = parseInt(newCount, 10);
localStorage.setItem('itemsPerPage', newCount);
window.currentPage = 1; window.currentPage = 1;
renderFileTable(window.currentFolder); if (window.viewMode === 'gallery') {
renderGalleryView(window.currentFolder);
} else {
renderFileTable(window.currentFolder);
}
}; };
// fileListView.js (bottom) // fileListView.js (bottom)

View File

@@ -5,6 +5,7 @@
// filtering the file list by tag, and persisting tag data. // filtering the file list by tag, and persisting tag data.
import { escapeHTML } from './domUtils.js'; import { escapeHTML } from './domUtils.js';
import { t } from './i18n.js'; import { t } from './i18n.js';
import { renderFileTable, renderGalleryView } from './fileListView.js';
export function openTagModal(file) { export function openTagModal(file) {
// Create the modal element. // Create the modal element.
@@ -63,6 +64,11 @@ export function openTagModal(file) {
updateTagModalDisplay(file); updateTagModalDisplay(file);
updateFileRowTagDisplay(file); updateFileRowTagDisplay(file);
saveFileTags(file); saveFileTags(file);
if (window.viewMode === 'gallery') {
renderGalleryView(window.currentFolder);
} else {
renderFileTable(window.currentFolder);
}
document.getElementById('tagNameInput').value = ''; document.getElementById('tagNameInput').value = '';
updateCustomTagDropdown(); updateCustomTagDropdown();
}); });
@@ -125,6 +131,11 @@ export function openMultiTagModal(files) {
saveFileTags(file); saveFileTags(file);
}); });
modal.remove(); modal.remove();
if (window.viewMode === 'gallery') {
renderGalleryView(window.currentFolder);
} else {
renderFileTable(window.currentFolder);
}
}); });
} }