320 lines
11 KiB
JavaScript
320 lines
11 KiB
JavaScript
// Creates and manages image and video DOM elements for the Image Drawer list.
|
|
// Handles file URL construction via the /jnodes_view_image API, video playback, and modal display.
|
|
|
|
import { api } from "/scripts/api.js";
|
|
import { $el } from "/scripts/ui.js";
|
|
|
|
import { utilitiesInstance, getBrowserVideoExtensions } from "../../common/Utilities.js";
|
|
|
|
import { ModalManager, ModalOptions } from "../../common/ModalManager.js";
|
|
|
|
import { setting_VideoPlaybackOptions } from "../../common/SettingsManager.js";
|
|
import { setVideoPlaybackRate, setVideoVolume, toggleVideoPlayback } from "../../common/VideoControl.js";
|
|
|
|
import * as ImageElementUtils from "./ImageListChildElementUtils.js";
|
|
|
|
export async function createImageElementFromFileInfo(fileInfo, imageDrawerInstance) {
|
|
if (!fileInfo) { return; }
|
|
let href = `/jnodes_view_image?`;
|
|
if (fileInfo.filename) {
|
|
href += `filename=${encodeURIComponent(fileInfo.filename)}&`;
|
|
}
|
|
if (fileInfo.type) {
|
|
href += `type=${fileInfo.type}&`;
|
|
}
|
|
if (fileInfo.subdirectory || fileInfo.subfolder) {
|
|
href += `subfolder=${encodeURIComponent(fileInfo.subdirectory || fileInfo.subfolder || "")}&`;
|
|
}
|
|
|
|
href += `t=${+new Date()}`; // Add Timestamp
|
|
|
|
fileInfo.imageHref = href;
|
|
|
|
// Fetch acceptable video types from python
|
|
const browserVideoExtensions = await getBrowserVideoExtensions();
|
|
const fileExtension = fileInfo.filename.split('.').pop().toLowerCase();
|
|
const bIsVideoFormat = fileInfo.file?.is_video || browserVideoExtensions.includes(fileExtension);
|
|
|
|
const imageElement =
|
|
$el("div.imageElement", {
|
|
bComplete: false,
|
|
style: {
|
|
borderRadius: '4px',
|
|
transition: "100ms",
|
|
}
|
|
});
|
|
|
|
imageElement.fileInfo = fileInfo;
|
|
imageElement.bIsVideoFormat = bIsVideoFormat;
|
|
imageElement.imageDrawerInstance = imageDrawerInstance;
|
|
|
|
imageElement.deleteItem = async function (bAlsoRemoveFromImageList = true, bNotifyImageListChanged = true) {
|
|
|
|
const deleteCall = imageElement.fileInfo.imageHref.replace("jnodes_view_image", "jnodes_delete_item");
|
|
const response = await api.fetchApi(deleteCall, { method: "DELETE" });
|
|
|
|
let jsonResponse;
|
|
try {
|
|
const decodedString = await utilitiesInstance.decodeReadableStream(response.body);
|
|
jsonResponse = JSON.parse(decodedString)
|
|
} catch (error) { console.error("Could not parse json from response."); }
|
|
|
|
let bSuccess = jsonResponse && jsonResponse.success && jsonResponse.success == true;
|
|
|
|
if (bAlsoRemoveFromImageList) {
|
|
imageElement.removeItemFromImageList(bNotifyImageListChanged);
|
|
}
|
|
|
|
return bSuccess;
|
|
};
|
|
|
|
imageElement.copyItem = async function (destinationDirectory) {
|
|
|
|
const destination = utilitiesInstance.joinPaths([destinationDirectory, fileInfo.filename]);
|
|
const copyCall = imageElement.fileInfo.imageHref.replace("jnodes_view_image", "jnodes_copy_item") + `&destination=${encodeURIComponent(destination)}`;
|
|
const response = await api.fetchApi(copyCall, { method: "POST" });
|
|
|
|
let jsonResponse;
|
|
try {
|
|
const decodedString = await utilitiesInstance.decodeReadableStream(response.body);
|
|
jsonResponse = JSON.parse(decodedString)
|
|
} catch (error) { console.error("Could not parse json from response."); }
|
|
|
|
let bSuccess = jsonResponse && jsonResponse.success && jsonResponse.success == true;
|
|
|
|
return bSuccess;
|
|
};
|
|
|
|
imageElement.removeItemFromImageList = async function (bNotifyImageListChanged = true) {
|
|
|
|
const imageDrawerListInstance = imageDrawerInstance.getComponentByName("ImageDrawerList");
|
|
|
|
if (bNotifyImageListChanged) {
|
|
imageDrawerListInstance.notifyStartChangingImageList();
|
|
}
|
|
|
|
imageDrawerListInstance.removeElementFromImageList(imageElement); // If it was deleted, remove it from the list
|
|
|
|
if (bNotifyImageListChanged) {
|
|
imageDrawerListInstance.notifyFinishChangingImageList();
|
|
}
|
|
};
|
|
|
|
const img = $el(bIsVideoFormat ? "video" : "img", {
|
|
// Store the image source as a data attribute for easy access
|
|
dataSrc: href,
|
|
preload: fileInfo.bShouldForceLoad ? "auto" : "metadata",
|
|
lastSeekTime: 0.0,
|
|
style: {
|
|
transition: "100ms",
|
|
cursor: bIsVideoFormat ? "default" : "pointer",
|
|
pointerEvents: "none",
|
|
},
|
|
onload: () => { ImageElementUtils.onLoadImageElement(imageElement); }, // Still / animated images
|
|
onloadedmetadata: () => { ImageElementUtils.onLoadImageElement(imageElement); }, // Videos
|
|
});
|
|
|
|
imageElement.img = img;
|
|
|
|
img.forceLoad = function () {
|
|
img.src = img.dataSrc;
|
|
|
|
if (bIsVideoFormat) {
|
|
setVideoPlaybackRate(img, setting_VideoPlaybackOptions.value.defaultPlaybackRate); // This gets reset when src is reset
|
|
}
|
|
};
|
|
|
|
imageElement.forceLoad = function () {
|
|
img.forceLoad();
|
|
};
|
|
|
|
if (fileInfo.bShouldForceLoad) {
|
|
// imageElement.forceLoad(); // Immediately load img if we don't want to lazy load (like in feed)
|
|
}
|
|
|
|
// Placeholder dimensions
|
|
if (fileInfo.file?.metadata_read) {
|
|
if (!imageElement.displayData) {
|
|
imageElement.displayData = {};
|
|
}
|
|
imageElement.displayData.FileDimensions = fileInfo.file.dimensions;
|
|
|
|
imageElement.displayData.AspectRatio = imageElement.displayData.FileDimensions[0] / imageElement.displayData.FileDimensions[1];
|
|
imageElement.style.aspectRatio = imageElement.displayData.AspectRatio;
|
|
} else {
|
|
// If we can't properly placehold, load the whole image now instead of later
|
|
imageElement.forceLoad();
|
|
}
|
|
|
|
if (!imageElement.displayData) {
|
|
imageElement.displayData = {};
|
|
}
|
|
|
|
if (bIsVideoFormat) {
|
|
|
|
img.initVideo = function () {
|
|
|
|
img.type = fileInfo.file?.format || undefined;
|
|
img.autoplay = false; // Start false, will autoplay via observer
|
|
img.loop = setting_VideoPlaybackOptions.value.loop;
|
|
img.controls = setting_VideoPlaybackOptions.value.controls;
|
|
img.muted = setting_VideoPlaybackOptions.value.muted;
|
|
setVideoVolume(img, setting_VideoPlaybackOptions.value.defaultVolume);
|
|
setVideoPlaybackRate(img, setting_VideoPlaybackOptions.value.defaultPlaybackRate);
|
|
}
|
|
|
|
img.addEventListener('wheel', (event) => {
|
|
|
|
if (setting_VideoPlaybackOptions.value.useWheelSeek ||
|
|
(!setting_VideoPlaybackOptions.value.useWheelSeek && event.altKey)) {
|
|
// Prevent scrolling the list
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
img.initVideo();
|
|
}
|
|
|
|
imageElement.appendChild(img);
|
|
|
|
// Sorting meta information
|
|
imageElement.filename = imageElement.friendlyName = fileInfo.filename;
|
|
imageElement.fileType = imageElement.filename.split(".")[1];
|
|
imageElement.file_age = fileInfo.file?.file_age || utilitiesInstance.getCurrentSecondsFromEpoch();
|
|
imageElement.subdirectory = fileInfo.subdirectory || null;
|
|
imageElement.path = fileInfo.subdirectory ? `${fileInfo.subdirectory}/${fileInfo.filename}` : fileInfo.filename;
|
|
imageElement.displayData.FileSize = fileInfo.file?.file_size || -1;
|
|
|
|
if (fileInfo?.file?.duration_in_seconds && fileInfo.file.duration_in_seconds > 0) {
|
|
imageElement.displayData.DurationInSeconds = fileInfo.file.duration_in_seconds;
|
|
}
|
|
if (fileInfo?.file?.fps && fileInfo.file.fps > 0) {
|
|
imageElement.displayData.FramesPerSecond = fileInfo.file.fps;
|
|
}
|
|
if (fileInfo?.file?.frame_count && fileInfo.file.frame_count > 0) {
|
|
imageElement.displayData.FrameCount = fileInfo.file.frame_count;
|
|
}
|
|
|
|
imageElement.displayData = utilitiesInstance.sortJsonObjectByKeys(imageElement.displayData);
|
|
|
|
imageElement.searchTerms = `${imageElement.filename} ${imageElement.subdirectory} ${JSON.stringify(imageElement.displayData)} `; // Search terms to start with, onload will add more
|
|
|
|
// Add meta keys as well, except workflow and prompt from comfy
|
|
if (fileInfo.file?.hasOwnProperty("metadata")) {
|
|
for (const key of Object.keys(fileInfo.file.metadata)) {
|
|
|
|
if (key == "prompt" || key == "workflow") {
|
|
continue;
|
|
}
|
|
imageElement.searchTerms += fileInfo.file.metadata[key];
|
|
}
|
|
}
|
|
|
|
imageElement.searchTerms = imageElement.searchTerms.toLowerCase().trim();
|
|
|
|
imageElement.getSearchTerms = function() { return imageElement.searchTerms; }
|
|
|
|
// Mouse Events
|
|
imageElement.bHasEverMousedOver = false;
|
|
imageElement.addEventListener("pointerover", (event) => {
|
|
|
|
if (!imageElement.bHasEverMousedOver) {
|
|
imageElement.addEventListener("pointerleave", (event) => {
|
|
|
|
ImageElementUtils.imageElementMouseLeaveEvent(event, imageElement);
|
|
});
|
|
|
|
imageElement.draggable = true;
|
|
imageElement.addEventListener('dragstart', function (event) {
|
|
fileInfo.displayData = imageElement.displayData;
|
|
fileInfo.metadata = imageElement.metadata;
|
|
event.dataTransfer.setData('text/jnodes_image_drawer_payload', `${JSON.stringify(fileInfo)}`);
|
|
ImageElementUtils.removeAndHideToolButtonFromImageElement(imageElement);
|
|
ImageElementUtils.hideImageElementCheckboxSelector(imageElement);
|
|
ImageElementUtils.hideToolTip();
|
|
});
|
|
|
|
imageElement.showInFileManager = async function () {
|
|
|
|
const call = imageElement.fileInfo.imageHref.replace("jnodes_view_image", "jnodes_request_open_file_manager");
|
|
api.fetchApi(call, { method: "POST" });
|
|
}
|
|
|
|
let clickTimeout = null;
|
|
|
|
img.addEventListener("click", async (e) => {
|
|
e.preventDefault();
|
|
|
|
if (bIsVideoFormat) {
|
|
|
|
if (clickTimeout) { clearTimeout(clickTimeout); clickTimeout = null; }
|
|
|
|
clickTimeout = setTimeout(() => {
|
|
clickTimeout = null;
|
|
if (img && toggleVideoPlayback) {
|
|
toggleVideoPlayback(img);
|
|
}
|
|
}, 250);
|
|
|
|
} else {
|
|
|
|
if (clickTimeout) { clearTimeout(clickTimeout); clickTimeout = null; }
|
|
|
|
clickTimeout = setTimeout(() => {
|
|
clickTimeout = null;
|
|
|
|
// Make a modal for the image, passing in its current index to allow for slideshows and image switching
|
|
const imageDrawerListInstance = imageDrawerInstance.getComponentByName("ImageDrawerList");
|
|
// Find this imageElement in the list
|
|
const currentIndex = Array.from(imageDrawerListInstance.getVisibleImageListChildren()).findIndex((op => op === imageElement));
|
|
|
|
const modalManager = new ModalManager(
|
|
imageDrawerInstance, new ModalOptions(
|
|
true,
|
|
(currentIndex !== undefined && currentIndex > -1) ? currentIndex : undefined),
|
|
true
|
|
);
|
|
|
|
modalManager.createModal(modalManager.createModalReadyImage(href));
|
|
|
|
// Remove focus from the currently focused element
|
|
document.activeElement.blur();
|
|
}, 250);
|
|
}
|
|
});
|
|
|
|
img.addEventListener("dblclick", async (e) => {
|
|
e.preventDefault();
|
|
|
|
if (clickTimeout) { clearTimeout(clickTimeout); clickTimeout = null; }
|
|
|
|
const imageDrawerListInstance = imageDrawerInstance.getComponentByName("ImageDrawerList");
|
|
const currentIndex = Array.from(imageDrawerListInstance.getVisibleImageListChildren()).findIndex((op => op === imageElement));
|
|
|
|
const modalManager = new ModalManager(
|
|
imageDrawerInstance, new ModalOptions(
|
|
true,
|
|
(currentIndex !== undefined && currentIndex > -1) ? currentIndex : undefined),
|
|
true
|
|
);
|
|
|
|
const modalContent = bIsVideoFormat
|
|
? modalManager.createModalReadyVideo(href)
|
|
: modalManager.createModalReadyImage(href);
|
|
modalManager.createModal(modalContent);
|
|
|
|
document.activeElement.blur();
|
|
});
|
|
|
|
imageElement.bHasEverMousedOver = true;
|
|
}
|
|
|
|
ImageElementUtils.imageElementMouseOverEvent(event, imageElement);
|
|
});
|
|
|
|
// Selection
|
|
ImageElementUtils.addCheckboxSelectorToImageElement(imageElement);
|
|
ImageElementUtils.hideImageElementCheckboxSelector(imageElement);
|
|
|
|
return imageElement;
|
|
} |