Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/wwwroot/css/genpage.css
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,32 @@ body {
display: inline-block;
cursor: pointer;
}
.browser-entry-has-checkbox {
position: relative;
}
.browser-entry-checkbox-wrap {
position: absolute;
top: 0.15rem;
left: 0.2rem;
width: 1.2rem;
height: 1.2rem;
z-index: 4;
border-radius: 0.2rem;
background-color: color-mix(in srgb, var(--background) 65%, transparent);
display: flex;
align-items: center;
justify-content: center;
}
.browser-entry-checkbox {
margin: 0;
width: 0.95rem;
height: 0.95rem;
cursor: pointer;
}
.browser-entry-selected {
outline: 2px solid var(--box-selected-border-stronger);
background-color: color-mix(in srgb, var(--box-selected-background) 70%, transparent);
}
.image-block img, .img-block video {
display: block;
margin: auto;
Expand Down Expand Up @@ -576,6 +602,24 @@ body {
background-color: var(--button-background-disabled);
color: var(--button-foreground-disabled);
}
.image-history-bulk-controls {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
margin-left: 0.5rem;
gap: 0.2rem;
}
.image-history-selected-count {
min-width: 5.5rem;
text-align: center;
font-size: 90%;
}
#image_history_delete_selected.interrupt-button {
min-width: auto;
margin-left: 0.2rem;
padding-left: 0.4rem;
padding-right: 0.4rem;
}
.tool-container .tool {
display: none;
}
Expand Down
238 changes: 213 additions & 25 deletions src/wwwroot/js/genpage/gentab/outputhistory.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,190 @@
let imageHistorySelected = new Set();
let imageHistoryBulkDeleteRunning = false;

function getImageHistoryEntries() {
let historySection = document.getElementById('imagehistorybrowser-content');
if (!historySection) {
return [];
}
return Array.from(historySection.children).filter(c => c.dataset?.name);
}

function pruneImageHistorySelectionToCurrentFiles() {
if (!imageHistoryBrowser?.lastFiles) {
return;
}
let currentFiles = new Set(imageHistoryBrowser.lastFiles.map(f => f.name));
for (let path of imageHistorySelected) {
if (!currentFiles.has(path)) {
imageHistorySelected.delete(path);
}
}
}

function updateImageHistoryBulkControls() {
let controls = document.getElementById('image_history_bulk_controls');
if (!controls) {
return;
}
let canDelete = permissions.hasPermission('user_delete_image');
controls.style.display = canDelete ? '' : 'none';
if (!canDelete) {
return;
}
let count = imageHistorySelected.size;
let countElem = document.getElementById('image_history_selected_count');
if (countElem) {
countElem.innerText = `${count} selected`;
}
let selectAllButton = document.getElementById('image_history_select_all');
let clearButton = document.getElementById('image_history_clear_selection');
let deleteButton = document.getElementById('image_history_delete_selected');
let anyEntries = getImageHistoryEntries().length > 0;
if (selectAllButton) {
selectAllButton.disabled = !anyEntries || imageHistoryBulkDeleteRunning;
}
if (clearButton) {
clearButton.disabled = count == 0 || imageHistoryBulkDeleteRunning;
}
if (deleteButton) {
deleteButton.disabled = count == 0 || imageHistoryBulkDeleteRunning;
}
}

function setImageHistorySelection(fullsrc, isSelected, entry = null) {
if (isSelected) {
imageHistorySelected.add(fullsrc);
}
else {
imageHistorySelected.delete(fullsrc);
}
if (!entry) {
entry = getImageHistoryEntries().find(e => e.dataset.name == fullsrc);
}
if (entry) {
entry.classList.toggle('browser-entry-selected', isSelected);
let checkbox = entry.querySelector('.browser-entry-checkbox');
if (checkbox) {
checkbox.checked = isSelected;
}
}
updateImageHistoryBulkControls();
}

function clearImageHistorySelection() {
imageHistorySelected.clear();
for (let entry of getImageHistoryEntries()) {
entry.classList.remove('browser-entry-selected');
let checkbox = entry.querySelector('.browser-entry-checkbox');
if (checkbox) {
checkbox.checked = false;
}
}
updateImageHistoryBulkControls();
}

function ensureImageHistoryBulkControlsReady() {
let controls = document.getElementById('image_history_bulk_controls');
if (!controls || controls.dataset.ready) {
updateImageHistoryBulkControls();
return;
}
controls.dataset.ready = 'true';
getRequiredElementById('image_history_select_all').addEventListener('click', () => {
for (let entry of getImageHistoryEntries()) {
setImageHistorySelection(entry.dataset.name, true, entry);
}
updateImageHistoryBulkControls();
});
getRequiredElementById('image_history_clear_selection').addEventListener('click', () => {
clearImageHistorySelection();
});
getRequiredElementById('image_history_delete_selected').addEventListener('click', () => {
deleteSelectedHistoryImages();
});
updateImageHistoryBulkControls();
}

function removeImageFromHistoryUI(fullsrc, src, explicitEntry = null) {
imageHistorySelected.delete(fullsrc);
let historySection = document.getElementById('imagehistorybrowser-content');
if (historySection) {
let entry = explicitEntry || getImageHistoryEntries().find(e => e.dataset.name == fullsrc || e.dataset.name == src);
if (entry) {
entry.remove();
}
}
let currentImage = currentImageHelper.getCurrentImage();
if (currentImage && currentImage.dataset.src == src) {
setCurrentImage(null);
}
let currentBatch = document.getElementById('current_image_batch');
if (currentBatch) {
let batchEntry = Array.from(currentBatch.children).find(e => e.dataset?.src == src);
if (batchEntry) {
removeImageBlockFromBatch(batchEntry);
}
}
updateImageHistoryBulkControls();
}

function deleteSingleHistoryImage(fullsrc, src, explicitEntry = null, errorHandle = null) {
return new Promise(resolve => {
let onSuccess = () => {
removeImageFromHistoryUI(fullsrc, src, explicitEntry);
resolve({ success: true });
};
genericRequest('DeleteImage', { 'path': fullsrc }, onSuccess, 0, error => {
if (errorHandle) {
errorHandle(error);
}
else {
showError(error);
}
resolve({ success: false, error });
});
});
}

async function deleteSelectedHistoryImages() {
if (imageHistoryBulkDeleteRunning) {
return;
}
let selected = [...imageHistorySelected];
if (selected.length == 0) {
return;
}
let imgWord = selected.length == 1 ? 'image' : 'images';
if (!uiImprover.lastShift && getUserSetting('ui.checkifsurebeforedelete', true) && !confirm(`Are you sure you want to delete ${selected.length} ${imgWord}?\nHold shift to bypass.`)) {
return;
}
imageHistoryBulkDeleteRunning = true;
updateImageHistoryBulkControls();
let deleted = 0;
let failed = 0;
for (let fullsrc of selected) {
let src = `${getImageOutPrefix()}/${fullsrc}`;
let res = await deleteSingleHistoryImage(fullsrc, src, null, () => {});
if (res.success) {
deleted++;
}
else {
failed++;
console.log(`Failed to delete image '${fullsrc}': ${res.error}`);
}
}
imageHistoryBulkDeleteRunning = false;
updateImageHistoryBulkControls();
if (deleted > 0) {
imageHistoryBrowser.lightRefresh();
}
if (failed > 0) {
showError(`Deleted ${deleted} image(s). Failed to delete ${failed} image(s).`);
}
else if (deleted > 0) {
doNoticePopover(`Deleted ${deleted} image${deleted == 1 ? '' : 's'}.`, 'notice-pop-green');
}
}

function listOutputHistoryFolderAndFiles(path, isRefresh, callback, depth) {
let sortBy = localStorage.getItem('image_history_sort_by') ?? 'Name';
Expand All @@ -11,6 +198,7 @@ function listOutputHistoryFolderAndFiles(path, isRefresh, callback, depth) {
sortBy = sortElem.value;
reverse = sortReverseElem.checked;
allowAnims = allowAnimsElem.checked;
ensureImageHistoryBulkControlsReady();
}
else { // first call happens before headers are built atm
fix = () => {
Expand All @@ -31,6 +219,7 @@ function listOutputHistoryFolderAndFiles(path, isRefresh, callback, depth) {
localStorage.setItem('image_history_allow_anims', allowAnimsElem.checked);
imageHistoryBrowser.lightRefresh();
});
ensureImageHistoryBulkControlsReady();
}
}
let prefix = path == '' ? '' : (path.endsWith('/') ? path : `${path}/`);
Expand All @@ -55,7 +244,7 @@ function listOutputHistoryFolderAndFiles(path, isRefresh, callback, depth) {

function buttonsForImage(fullsrc, src, metadata) {
let isDataImage = src.startsWith('data:');
buttons = [];
let buttons = [];
if (permissions.hasPermission('user_star_images') && !isDataImage) {
buttons.push({
label: (metadata && JSON.parse(metadata).is_starred) ? 'Unstar' : 'Star',
Expand Down Expand Up @@ -104,28 +293,7 @@ function buttonsForImage(fullsrc, src, metadata) {
if (!shifted) {
imageFullView.close();
}
genericRequest('DeleteImage', {'path': fullsrc}, data => {
if (e) {
e.remove();
}
let historySection = getRequiredElementById('imagehistorybrowser-content');
let div = historySection.querySelector(`.image-block[data-name="${fullsrc}"]`);
if (div) {
div.remove();
}
div = historySection.querySelector(`.image-block[data-name="${src}"]`);
if (div) {
div.remove();
}
let currentImage = currentImageHelper.getCurrentImage();
if (currentImage && currentImage.dataset.src == src) {
setCurrentImage(null);
}
div = getRequiredElementById('current_image_batch').querySelector(`.image-block[data-src="${src}"]`);
if (div) {
removeImageBlockFromBatch(div);
}
});
deleteSingleHistoryImage(fullsrc, src, e);
}
});
}
Expand All @@ -134,6 +302,8 @@ function buttonsForImage(fullsrc, src, metadata) {

function describeOutputFile(image) {
let buttons = buttonsForImage(image.data.fullsrc, image.data.src, image.data.metadata);
let canDelete = permissions.hasPermission('user_delete_image') && !image.data.src.startsWith('data:');
let isSelected = imageHistorySelected.has(image.data.fullsrc);
let parsedMeta = { is_starred: false };
if (image.data.metadata) {
let metadata = image.data.metadata;
Expand Down Expand Up @@ -164,7 +334,18 @@ function describeOutputFile(image) {
let searchable = `${image.data.name}, ${image.data.metadata}, ${image.data.fullsrc}`;
let detail_list = [escapeHtml(image.data.name), formattedMetadata.replaceAll('<br>', '&emsp;')];
let aspectRatio = parsedMeta.sui_image_params?.width && parsedMeta.sui_image_params?.height ? parsedMeta.sui_image_params.width / parsedMeta.sui_image_params.height : null;
return { name, description, buttons, 'image': imageSrc, 'dragimage': dragImage, className: parsedMeta.is_starred ? 'image-block-starred' : '', searchable, display: name, detail_list, aspectRatio };
let className = parsedMeta.is_starred ? 'image-block-starred' : '';
if (isSelected) {
className = `${className} browser-entry-selected`.trim();
}
let checkbox = canDelete ? {
checked: isSelected,
title: 'Select image',
onchange: (checked, file, div) => {
setImageHistorySelection(image.data.fullsrc, checked, div);
}
} : null;
return { name, description, buttons, checkbox, 'image': imageSrc, 'dragimage': dragImage, className, searchable, display: name, detail_list, aspectRatio };
}

function selectOutputInHistory(image, div) {
Expand All @@ -189,7 +370,14 @@ function selectOutputInHistory(image, div) {
}

let imageHistoryBrowser = new GenPageBrowserClass('image_history', listOutputHistoryFolderAndFiles, 'imagehistorybrowser', 'Thumbnails', describeOutputFile, selectOutputInHistory,
`<label for="image_history_sort_by">Sort:</label> <select id="image_history_sort_by"><option>Name</option><option>Date</option></select> <input type="checkbox" id="image_history_sort_reverse"> <label for="image_history_sort_reverse">Reverse</label> &emsp; <input type="checkbox" id="image_history_allow_anims" checked autocomplete="off"> <label for="image_history_allow_anims">Allow Animation</label>`);
`<label for="image_history_sort_by">Sort:</label> <select id="image_history_sort_by"><option>Name</option><option>Date</option></select> <input type="checkbox" id="image_history_sort_reverse"> <label for="image_history_sort_reverse">Reverse</label> &emsp; <input type="checkbox" id="image_history_allow_anims" checked autocomplete="off"> <label for="image_history_allow_anims">Allow Animation</label> <span id="image_history_bulk_controls" class="image-history-bulk-controls"><span id="image_history_selected_count" class="image-history-selected-count">0 selected</span> <button id="image_history_select_all" class="refresh-button">Select All</button> <button id="image_history_clear_selection" class="refresh-button">Clear</button> <button id="image_history_delete_selected" class="interrupt-button">Delete Selected</button></span>`);
imageHistoryBrowser.folderSelectedEvent = () => {
clearImageHistorySelection();
};
imageHistoryBrowser.builtEvent = () => {
pruneImageHistorySelectionToCurrentFiles();
updateImageHistoryBulkControls();
};

function storeImageToHistoryWithCurrentParams(img) {
let data = getGenInput();
Expand Down
25 changes: 25 additions & 0 deletions src/wwwroot/js/genpage/helpers/browsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,9 @@ class GenPageBrowserClass {
break;
}
let div = createDiv(null, `${desc.className}`);
if (desc.checkbox) {
div.classList.add('browser-entry-has-checkbox');
}
let popoverId = `${this.id}-${id}`;
if (desc.buttons.length > 0) {
let menuDiv = createDiv(`popover_${popoverId}`, 'sui-popover sui_popover_model');
Expand Down Expand Up @@ -457,6 +460,28 @@ class GenPageBrowserClass {
this.select(file, div);
});
div.appendChild(img);
if (desc.checkbox) {
let checkboxWrap = createSpan(null, 'browser-entry-checkbox-wrap');
checkboxWrap.addEventListener('click', (e) => {
e.stopPropagation();
});
let checkbox = document.createElement('input');
checkbox.className = 'browser-entry-checkbox';
checkbox.type = 'checkbox';
checkbox.checked = !!desc.checkbox.checked;
checkbox.title = desc.checkbox.title || 'Select';
checkbox.addEventListener('click', (e) => {
e.stopPropagation();
});
checkbox.addEventListener('change', (e) => {
e.stopPropagation();
if (desc.checkbox.onchange) {
desc.checkbox.onchange(checkbox.checked, file, div, checkbox);
}
});
checkboxWrap.appendChild(checkbox);
div.appendChild(checkboxWrap);
}
if (this.format.includes('Cards')) {
div.className += ' model-block model-block-hoverable';
if (this.format.startsWith('Small')) { div.classList.add('model-block-small'); }
Expand Down