Gallery View add selection actions and search filtering
This commit is contained in:
20
CHANGELOG.md
20
CHANGELOG.md
@@ -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 re‑render the view‑toggle button on gallery load
|
||||||
|
- Restore per‑card 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`
|
||||||
|
- Re‑render the list after saving a single‑file tag
|
||||||
|
- Re‑render the list after saving multi‑file tags
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 4/17/2025
|
## Changes 4/17/2025
|
||||||
|
|
||||||
- Generate OpenAPI spec and API HTML docs
|
- Generate OpenAPI spec and API HTML docs
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user