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 = ` `; 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 = `
Loading...
`; // 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 deletePathBtn = container.querySelector('.kiko-delete-path'); const browseBtn = container.querySelector('.kiko-browse'); const upFolderBtn = container.querySelector('.kiko-up-folder'); const hideDotFolders = container.querySelector('.kiko-hide-dot-folders'); 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'); const autocompleteDropdown = container.querySelector('.kiko-autocomplete-dropdown'); 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); // Autocomplete functionality let autocompleteSelectedIndex = -1; let autocompleteSuggestions = []; const smallFolderSVG = ``; async function fetchDirectorySuggestions(path) { try { const response = await api.fetchApi(`/kiko_local_image_loader/list_directories?path=${encodeURIComponent(path)}`); const data = await response.json(); return data.directories || []; } catch (error) { console.error('Error fetching directories:', error); return []; } } function renderAutocompleteSuggestions(suggestions) { autocompleteSuggestions = suggestions; autocompleteSelectedIndex = -1; if (suggestions.length === 0) { autocompleteDropdown.classList.remove('visible'); autocompleteDropdown.innerHTML = ''; return; } autocompleteDropdown.innerHTML = suggestions.map((path, index) => { const parts = path.split('/').filter(p => p); const folderName = parts.pop() || path; const parentPath = parts.length > 0 ? '/' + parts.join('/') : ''; return `
${smallFolderSVG}
${folderName} ${parentPath ? ` — ${parentPath}` : ''}
`; }).join(''); autocompleteDropdown.classList.add('visible'); // Add click handlers to items autocompleteDropdown.querySelectorAll('.kiko-autocomplete-item').forEach(item => { item.addEventListener('mousedown', (e) => { e.preventDefault(); // Prevent blur from hiding dropdown before click registers const path = item.dataset.path; selectAutocompleteSuggestion(path); }); }); } function hideAutocomplete() { autocompleteDropdown.classList.remove('visible'); autocompleteDropdown.innerHTML = ''; autocompleteSuggestions = []; autocompleteSelectedIndex = -1; } function selectAutocompleteSuggestion(path) { pathInput.value = path + '/'; hideAutocomplete(); // Trigger another fetch to show contents of selected directory debouncedAutocomplete(path + '/'); } function updateAutocompleteSelection() { const items = autocompleteDropdown.querySelectorAll('.kiko-autocomplete-item'); items.forEach((item, index) => { if (index === autocompleteSelectedIndex) { item.classList.add('selected'); item.scrollIntoView({ block: 'nearest' }); } else { item.classList.remove('selected'); } }); } const debouncedAutocomplete = debounce(async (value) => { const suggestions = await fetchDirectorySuggestions(value); renderAutocompleteSuggestions(suggestions); }, 150); // Path input event handlers for autocomplete pathInput.addEventListener('input', (e) => { const value = e.target.value; if (value.length > 0) { debouncedAutocomplete(value); } else { hideAutocomplete(); } }); pathInput.addEventListener('focus', () => { const value = pathInput.value; if (value.length > 0) { debouncedAutocomplete(value); } }); pathInput.addEventListener('blur', () => { // Delay hiding to allow click events to register setTimeout(hideAutocomplete, 150); }); pathInput.addEventListener('keydown', (e) => { if (!autocompleteDropdown.classList.contains('visible')) { if (e.key === 'Enter') { loadImages(pathInput.value); e.preventDefault(); } return; } switch (e.key) { case 'ArrowDown': e.preventDefault(); autocompleteSelectedIndex = Math.min(autocompleteSelectedIndex + 1, autocompleteSuggestions.length - 1); updateAutocompleteSelection(); break; case 'ArrowUp': e.preventDefault(); autocompleteSelectedIndex = Math.max(autocompleteSelectedIndex - 1, -1); updateAutocompleteSelection(); break; case 'Enter': e.preventDefault(); if (autocompleteSelectedIndex >= 0 && autocompleteSelectedIndex < autocompleteSuggestions.length) { selectAutocompleteSuggestion(autocompleteSuggestions[autocompleteSelectedIndex]); } else { hideAutocomplete(); loadImages(pathInput.value); } break; case 'Escape': hideAutocomplete(); break; case 'Tab': if (autocompleteSelectedIndex >= 0) { e.preventDefault(); selectAutocompleteSuggestion(autocompleteSuggestions[autocompleteSelectedIndex]); } else if (autocompleteSuggestions.length === 1) { e.preventDefault(); selectAutocompleteSuggestion(autocompleteSuggestions[0]); } break; } }); // 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); } }; // Delete selected saved path deletePathBtn.onclick = async () => { const selectedPath = pathPresets.value; if (!selectedPath) return; try { const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths'); const data = await response.json(); const savedPaths = data.saved_paths || []; const updatedPaths = savedPaths.filter(p => p !== selectedPath); if (updatedPaths.length !== savedPaths.length) { await api.fetchApi('/kiko_local_image_loader/save_paths', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths: updatedPaths }) }); await loadSavedPaths(); } } catch (error) { console.error('Error deleting 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: 'true', show_audio: 'true', hide_dot_folders: hideDotFolders.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 && !parentDirectory) { cardHolder.innerHTML = '
No items found
'; return; } // Create items array with parent folder at the beginning if it exists const itemsToRender = parentDirectory ? [{ type: 'parent_dir', name: '..', path: parentDirectory }, ...currentItems] : currentItems; itemsToRender.forEach((item, index) => { const card = document.createElement('div'); card.className = 'kiko-gallery-card'; if (item.type === 'parent_dir') { // Parent directory (..) with white up arrow on blue folder const upArrowSVG = ``; card.innerHTML = `
${upArrowSVG}
.. (Up)
`; card.onclick = () => { loadImages(item.path); }; } else if (item.type === 'dir') { card.innerHTML = `
${folderSVG}
${item.name}
`; 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}
${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}
${videoSVG}
${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 = `
${audioSVG}
${item.name}
`; // 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); pathPresets.onchange = () => { if (pathPresets.value) { loadImages(pathPresets.value); } }; hideDotFolders.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; }; } } });