diff --git a/web/kiko_local_image_loader.js b/web/kiko_local_image_loader.js
new file mode 100644
index 0000000..cdc1287
--- /dev/null
+++ b/web/kiko_local_image_loader.js
@@ -0,0 +1,755 @@
+import { app } from "/scripts/app.js";
+import { api } from "/scripts/api.js";
+
+// Setup global lightbox for image preview
+function setupGlobalLightbox() {
+ if (document.getElementById('kiko-image-lightbox')) return;
+
+ const lightboxId = 'kiko-image-lightbox';
+ const lightboxHTML = `
+
+
+
+
+
+
![Preview]()
+
+
+
+
+
+ `;
+
+ const lightboxCSS = `
+ #${lightboxId} {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.85);
+ display: none;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ box-sizing: border-box;
+ -webkit-user-select: none;
+ user-select: none;
+ }
+ #${lightboxId} .lightbox-content {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ }
+ #${lightboxId} img, #${lightboxId} video {
+ max-width: 95%;
+ max-height: 95%;
+ object-fit: contain;
+ transition: transform 0.1s ease-out;
+ transform: scale(1) translate(0, 0);
+ }
+ #${lightboxId} audio {
+ width: 80%;
+ max-width: 600px;
+ }
+ #${lightboxId} img {
+ cursor: grab;
+ }
+ #${lightboxId} img.panning {
+ cursor: grabbing;
+ }
+ #${lightboxId} .lightbox-close {
+ position: absolute;
+ top: 15px;
+ right: 20px;
+ width: 35px;
+ height: 35px;
+ background-color: rgba(0,0,0,0.5);
+ color: #fff;
+ border-radius: 50%;
+ border: 2px solid #fff;
+ font-size: 24px;
+ line-height: 30px;
+ text-align: center;
+ cursor: pointer;
+ z-index: 10002;
+ }
+ #${lightboxId} .lightbox-prev, #${lightboxId} .lightbox-next {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 45px;
+ height: 60px;
+ background-color: rgba(0,0,0,0.4);
+ color: #fff;
+ border: none;
+ font-size: 30px;
+ cursor: pointer;
+ z-index: 10001;
+ transition: background-color 0.2s;
+ }
+ #${lightboxId} .lightbox-prev:hover, #${lightboxId} .lightbox-next:hover {
+ background-color: rgba(0,0,0,0.7);
+ }
+ #${lightboxId} .lightbox-prev {
+ left: 15px;
+ }
+ #${lightboxId} .lightbox-next {
+ right: 15px;
+ }
+ #${lightboxId} [disabled] {
+ display: none;
+ }
+ #${lightboxId} .lightbox-dimensions {
+ position: absolute;
+ bottom: 0px;
+ left: 50%;
+ transform: translateX(-50%);
+ background-color: rgba(0, 0, 0, 0.7);
+ color: #fff;
+ padding: 2px 4px;
+ border-radius: 5px;
+ font-size: 14px;
+ z-index: 10001;
+ }
+ `;
+
+ document.body.insertAdjacentHTML('beforeend', lightboxHTML);
+ const styleEl = document.createElement('style');
+ styleEl.textContent = lightboxCSS;
+ document.head.appendChild(styleEl);
+}
+
+setupGlobalLightbox();
+
+app.registerExtension({
+ name: "KikoTools.LocalImageLoader",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === "KikoLocalImageLoader") {
+ const onNodeCreated = nodeType.prototype.onNodeCreated;
+ nodeType.prototype.onNodeCreated = function () {
+ const r = onNodeCreated?.apply(this, arguments);
+
+ const galleryContainer = document.createElement("div");
+ const uniqueId = `kiko-gallery-${Math.random().toString(36).substring(2, 9)}`;
+ galleryContainer.id = uniqueId;
+
+ const folderSVG = ``;
+ const videoSVG = ``;
+ const audioSVG = ``;
+
+ galleryContainer.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Add the widget to the node
+ this.galleryWidget = this.addDOMWidget("kiko_local_image_gallery", "div", galleryContainer, {
+ serialize: false,
+ });
+
+ // Set initial size for the node
+ this.size = [800, 600];
+ this.setSize(this.size);
+
+ // Initialize gallery functionality
+ const node = this;
+ const container = galleryContainer.querySelector('.kiko-container-wrapper');
+ const pathInput = container.querySelector('.kiko-path-input');
+ const pathPresets = container.querySelector('.kiko-path-presets');
+ const savePathBtn = container.querySelector('.kiko-save-path');
+ const browseBtn = container.querySelector('.kiko-browse');
+ const upFolderBtn = container.querySelector('.kiko-up-folder');
+ const showVideos = container.querySelector('.kiko-show-videos');
+ const showAudio = container.querySelector('.kiko-show-audio');
+ const sortBy = container.querySelector('.kiko-sort-by');
+ const sortOrder = container.querySelector('.kiko-sort-order');
+ const refreshBtn = container.querySelector('.kiko-refresh');
+ const cardHolder = container.querySelector('.kiko-cardholder');
+ const pagination = container.querySelector('.kiko-pagination');
+ const statusMessage = container.querySelector('.kiko-status-message');
+
+ let currentPage = 1;
+ let totalPages = 1;
+ let currentDirectory = '';
+ let parentDirectory = null;
+ let currentItems = [];
+ let selectedPaths = {
+ image: null,
+ video: null,
+ audio: null
+ };
+
+ // Utility: Debounce function
+ const debounce = (func, delay) => {
+ let timeoutId;
+ return (...args) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => func.apply(this, args), delay);
+ };
+ };
+
+ // Apply responsive masonry layout
+ const applyMasonryLayout = () => {
+ const minCardWidth = 120;
+ const gap = 5;
+ const containerWidth = cardHolder.clientWidth - 10; // Account for padding
+ if (containerWidth <= 0) return;
+
+ const columnCount = Math.max(1, Math.floor(containerWidth / (minCardWidth + gap)));
+ const totalGapSpace = (columnCount - 1) * gap;
+ const actualCardWidth = Math.floor((containerWidth - totalGapSpace) / columnCount);
+ const columnHeights = new Array(columnCount).fill(0);
+
+ const cards = cardHolder.querySelectorAll('.kiko-gallery-card');
+ cards.forEach(card => {
+ card.style.width = `${actualCardWidth}px`;
+ const minHeight = Math.min(...columnHeights);
+ const columnIndex = columnHeights.indexOf(minHeight);
+ card.style.left = `${columnIndex * (actualCardWidth + gap)}px`;
+ card.style.top = `${minHeight}px`;
+ columnHeights[columnIndex] += card.offsetHeight + gap;
+ });
+
+ // Set container height - let scrollbar handle overflow
+ const maxHeight = Math.max(...columnHeights);
+ if (maxHeight > 0) {
+ cardHolder.style.minHeight = `${Math.min(200, maxHeight)}px`;
+ }
+ };
+
+ const debouncedLayout = debounce(applyMasonryLayout, 20);
+
+ // Set up ResizeObserver for responsive layout
+ new ResizeObserver(debouncedLayout).observe(cardHolder);
+
+ // Load saved paths
+ async function loadSavedPaths() {
+ try {
+ const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths');
+ const data = await response.json();
+
+ pathPresets.innerHTML = '';
+ data.saved_paths?.forEach(path => {
+ const option = document.createElement('option');
+ option.value = path;
+ option.textContent = path.split('/').pop() || path;
+ option.title = path;
+ pathPresets.appendChild(option);
+ });
+ } catch (error) {
+ console.error('Error loading saved paths:', error);
+ }
+ }
+
+ // Save current path
+ savePathBtn.onclick = async () => {
+ if (!currentDirectory) return;
+
+ try {
+ const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths');
+ const data = await response.json();
+ const savedPaths = data.saved_paths || [];
+
+ if (!savedPaths.includes(currentDirectory)) {
+ savedPaths.push(currentDirectory);
+ await api.fetchApi('/kiko_local_image_loader/save_paths', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ paths: savedPaths })
+ });
+ await loadSavedPaths();
+ }
+ } catch (error) {
+ console.error('Error saving path:', error);
+ }
+ };
+
+ // Navigate to parent directory
+ upFolderBtn.onclick = () => {
+ if (parentDirectory) {
+ loadImages(parentDirectory);
+ }
+ };
+
+ // Load images from directory
+ async function loadImages(directory, page = 1) {
+ if (!directory) return;
+
+ statusMessage.style.display = 'block';
+ statusMessage.textContent = 'Loading...';
+
+ const params = new URLSearchParams({
+ directory: directory,
+ page: page.toString(),
+ per_page: '50',
+ show_videos: showVideos.checked,
+ show_audio: showAudio.checked,
+ sort_by: sortBy.value,
+ sort_order: sortOrder.value
+ });
+
+ try {
+ const response = await api.fetchApi(`/kiko_local_image_loader/images?${params}`);
+ const data = await response.json();
+
+ if (data.error) {
+ statusMessage.textContent = data.error;
+ return;
+ }
+
+ currentDirectory = data.current_directory;
+ parentDirectory = data.parent_directory;
+ currentPage = data.current_page;
+ totalPages = data.total_pages;
+ currentItems = data.items;
+
+ pathInput.value = currentDirectory;
+ upFolderBtn.disabled = !parentDirectory;
+
+ renderGallery();
+ renderPagination();
+
+ statusMessage.style.display = 'none';
+ } catch (error) {
+ console.error('Error loading images:', error);
+ statusMessage.textContent = 'Error loading directory';
+ }
+ }
+
+ // Render gallery cards
+ function renderGallery() {
+ cardHolder.innerHTML = '';
+
+ if (currentItems.length === 0) {
+ cardHolder.innerHTML = 'No items found
';
+ return;
+ }
+
+ currentItems.forEach((item, index) => {
+ const card = document.createElement('div');
+ card.className = 'kiko-gallery-card';
+
+ if (item.type === 'dir') {
+ card.innerHTML = `
+
+ `;
+ card.onclick = () => {
+ loadImages(item.path);
+ };
+ } else if (item.type === 'image') {
+ const thumbnailUrl = `/kiko_local_image_loader/thumbnail?filepath=${encodeURIComponent(item.path)}`;
+ card.innerHTML = `
+
+ ${item.name}
+ `;
+
+ // Trigger layout when image loads
+ const img = card.querySelector('img');
+ if (img) {
+ img.onload = debouncedLayout;
+ }
+
+ // Single click to select
+ card.onclick = () => selectMedia(item, 'image', card);
+
+ // Double click to open in new tab
+ card.ondblclick = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const fullImageUrl = `/kiko_local_image_loader/view?filepath=${encodeURIComponent(item.path)}`;
+ window.open(fullImageUrl, '_blank');
+ };
+ } else if (item.type === 'video') {
+ const thumbnailUrl = `/kiko_local_image_loader/thumbnail?filepath=${encodeURIComponent(item.path)}`;
+ card.innerHTML = `
+
+ ${item.name}
+ `;
+
+ const img = card.querySelector('img');
+ if (img) {
+ img.onload = debouncedLayout;
+ }
+
+ // Single click to select
+ card.onclick = () => selectMedia(item, 'video', card);
+
+ // Double click to open in new tab
+ card.ondblclick = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const fullVideoUrl = `/kiko_local_image_loader/view?filepath=${encodeURIComponent(item.path)}`;
+ window.open(fullVideoUrl, '_blank');
+ };
+ } else if (item.type === 'audio') {
+ card.innerHTML = `
+
+ `;
+
+ // Single click to select
+ card.onclick = () => selectMedia(item, 'audio', card);
+
+ // Double click to open in new tab
+ card.ondblclick = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const fullAudioUrl = `/kiko_local_image_loader/view?filepath=${encodeURIComponent(item.path)}`;
+ window.open(fullAudioUrl, '_blank');
+ };
+ }
+
+ // Check if selected
+ if (selectedPaths[item.type] === item.path) {
+ card.classList.add('kiko-selected');
+ }
+
+ cardHolder.appendChild(card);
+ });
+
+ // Apply layout after all cards are added
+ requestAnimationFrame(debouncedLayout);
+ }
+
+ // Select media
+ async function selectMedia(item, type, cardElement) {
+ // Update selection
+ selectedPaths[type] = item.path;
+
+ // Update UI
+ cardHolder.querySelectorAll('.kiko-gallery-card').forEach(card => {
+ card.classList.remove('kiko-selected');
+ });
+ cardElement.classList.add('kiko-selected');
+
+ // Send to backend
+ try {
+ await api.fetchApi('/kiko_local_image_loader/set_node_selection', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ node_id: node.id,
+ path: item.path,
+ type: type
+ })
+ });
+
+ // Trigger node update
+ node.setDirtyCanvas(true);
+ } catch (error) {
+ console.error('Error setting selection:', error);
+ }
+ }
+
+ // Render pagination
+ function renderPagination() {
+ pagination.innerHTML = '';
+
+ if (totalPages <= 1) return;
+
+ const createButton = (text, page) => {
+ const btn = document.createElement('button');
+ btn.textContent = text;
+ btn.style.cssText = 'background: #333; color: #ccc; border: 1px solid #555; padding: 1px 6px; cursor: pointer; font-size: 11px;';
+ if (page === currentPage) {
+ btn.style.background = '#555';
+ }
+ btn.onclick = () => loadImages(currentDirectory, page);
+ return btn;
+ };
+
+ if (currentPage > 1) {
+ pagination.appendChild(createButton('◀', currentPage - 1));
+ }
+
+ for (let i = 1; i <= totalPages; i++) {
+ if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
+ pagination.appendChild(createButton(i.toString(), i));
+ } else if (i === currentPage - 3 || i === currentPage + 3) {
+ const span = document.createElement('span');
+ span.textContent = '...';
+ span.style.padding = '0 5px';
+ pagination.appendChild(span);
+ }
+ }
+
+ if (currentPage < totalPages) {
+ pagination.appendChild(createButton('▶', currentPage + 1));
+ }
+ }
+
+ // Event handlers
+ browseBtn.onclick = () => loadImages(pathInput.value || currentDirectory);
+ refreshBtn.onclick = () => loadImages(currentDirectory, currentPage);
+
+ pathInput.onkeydown = (e) => {
+ if (e.key === 'Enter') {
+ loadImages(pathInput.value);
+ }
+ };
+
+ pathPresets.onchange = () => {
+ if (pathPresets.value) {
+ loadImages(pathPresets.value);
+ }
+ };
+
+ showVideos.onchange = () => loadImages(currentDirectory, 1);
+ showAudio.onchange = () => loadImages(currentDirectory, 1);
+ sortBy.onchange = () => loadImages(currentDirectory, 1);
+ sortOrder.onchange = () => loadImages(currentDirectory, 1);
+
+ // Load initial data
+ loadSavedPaths();
+
+ // Try to load last path
+ api.fetchApi('/kiko_local_image_loader/get_last_path').then(async response => {
+ const data = await response.json();
+ if (data.last_path) {
+ loadImages(data.last_path);
+ }
+ });
+
+ return r;
+ };
+ }
+ }
+});
\ No newline at end of file