From 53ab6d8ab456a63ee0df4cde67a44942883fd0d6 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Fri, 7 Mar 2025 09:11:00 +0800 Subject: [PATCH] Refactor LoraCard.js code --- static/js/components/LoraCard.js | 473 +----------------------------- static/js/components/LoraModal.js | 471 +++++++++++++++++++++++++++++ static/js/main.js | 3 +- 3 files changed, 474 insertions(+), 473 deletions(-) create mode 100644 static/js/components/LoraModal.js diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js index f59df52b..ee237af1 100644 --- a/static/js/components/LoraCard.js +++ b/static/js/components/LoraCard.js @@ -1,6 +1,6 @@ import { showToast } from '../utils/uiHelpers.js'; -import { modalManager } from '../managers/ModalManager.js'; import { state } from '../state/index.js'; +import { showLoraModal } from './LoraModal.js'; export function createLoraCard(lora) { const card = document.createElement('div'); @@ -129,475 +129,4 @@ export function createLoraCard(lora) { }); return card; -} - -export function showLoraModal(lora) { - const escapedWords = lora.civitai?.trainedWords?.length ? - lora.civitai.trainedWords.map(word => word.replace(/'/g, '\\\'')) : []; - - const content = ` - - `; - - modalManager.showModal('loraModal', content); - setupEditableFields(); - setupShowcaseScroll(); // Add this line -} - -// 添加复制文件名的函数 -window.copyFileName = async function(fileName) { - try { - await navigator.clipboard.writeText(fileName); - showToast('File name copied', 'success'); - } catch (err) { - console.error('Copy failed:', err); - showToast('Copy failed', 'error'); - } -}; - -function setupEditableFields() { - const editableFields = document.querySelectorAll('.editable-field [contenteditable]'); - - editableFields.forEach(field => { - field.addEventListener('focus', function() { - if (this.textContent === 'Add your notes here...' || - this.textContent === 'Save usage tips here..') { - this.textContent = ''; - } - }); - - field.addEventListener('blur', function() { - if (this.textContent.trim() === '') { - this.textContent = this.classList.contains('usage-tips-content') - ? 'Save usage tips here..' - : 'Add your notes here...'; - } - }); - }); - - const presetSelector = document.getElementById('preset-selector'); - const presetValue = document.getElementById('preset-value'); - const addPresetBtn = document.querySelector('.add-preset-btn'); - const presetTags = document.querySelector('.preset-tags'); - - presetSelector.addEventListener('change', function() { - const selected = this.value; - if (selected) { - presetValue.style.display = 'inline-block'; - presetValue.min = selected.includes('strength') ? 0 : 1; - presetValue.max = selected.includes('strength') ? 1 : 12; - presetValue.step = selected.includes('strength') ? 0.01 : 1; - if (selected === 'clip_skip') { - presetValue.type = 'number'; - presetValue.step = 1; - } - // Add auto-focus - setTimeout(() => presetValue.focus(), 0); - } else { - presetValue.style.display = 'none'; - } - }); - - addPresetBtn.addEventListener('click', async function() { - const key = presetSelector.value; - const value = presetValue.value; - - if (!key || !value) return; - - const filePath = document.querySelector('.modal-content') - .querySelector('.file-path').textContent + - document.querySelector('.modal-content') - .querySelector('#file-name').textContent + '.safetensors'; - - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - const currentPresets = parsePresets(loraCard.dataset.usage_tips); - - currentPresets[key] = parseFloat(value); - const newPresetsJson = JSON.stringify(currentPresets); - - await saveModelMetadata(filePath, { - usage_tips: newPresetsJson - }); - - loraCard.dataset.usage_tips = newPresetsJson; - presetTags.innerHTML = renderPresetTags(currentPresets); - - presetSelector.value = ''; - presetValue.value = ''; - presetValue.style.display = 'none'; - }); - - // Add keydown event listeners for notes - const notesContent = document.querySelector('.notes-content'); - if (notesContent) { - notesContent.addEventListener('keydown', async function(e) { - if (e.key === 'Enter') { - if (e.shiftKey) { - // Allow shift+enter for new line - return; - } - e.preventDefault(); - const filePath = document.querySelector('.modal-content') - .querySelector('.file-path').textContent + - document.querySelector('.modal-content') - .querySelector('#file-name').textContent + '.safetensors'; - await saveNotes(filePath); - } - }); - } - - // Add keydown event for preset value - presetValue.addEventListener('keydown', function(e) { - if (e.key === 'Enter') { - e.preventDefault(); - addPresetBtn.click(); - } - }); -} - -window.saveNotes = async function(filePath) { - const content = document.querySelector('.notes-content').textContent; - try { - await saveModelMetadata(filePath, { notes: content }); - - // Update the corresponding lora card's dataset - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - if (loraCard) { - loraCard.dataset.notes = content; - } - - showToast('Notes saved successfully', 'success'); - } catch (error) { - showToast('Failed to save notes', 'error'); - } -}; - -async function saveModelMetadata(filePath, data) { - const response = await fetch('/loras/api/save-metadata', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - file_path: filePath, - ...data - }) - }); - - if (!response.ok) { - throw new Error('Failed to save metadata'); - } -} - -function renderTriggerWords(words) { - if (!words.length) return ` -
- - No trigger word needed -
- `; - - return ` -
- -
- ${words.map(word => ` -
- ${word} - - - -
- `).join('')} -
-
- `; -} - -function renderShowcaseImages(images) { - if (!images?.length) return ''; - - return ` -
-
- - Scroll or click to show ${images.length} examples -
- - -
- `; -} - -// Add this to the window object for global access -export function toggleShowcase(element) { - const carousel = element.nextElementSibling; - const isCollapsed = carousel.classList.contains('collapsed'); - const indicator = element.querySelector('span'); - const icon = element.querySelector('i'); - - carousel.classList.toggle('collapsed'); - - if (isCollapsed) { - const count = carousel.querySelectorAll('.media-wrapper').length; - indicator.textContent = `Scroll or click to hide examples`; - icon.classList.replace('fa-chevron-down', 'fa-chevron-up'); - initLazyLoading(carousel); - } else { - const count = carousel.querySelectorAll('.media-wrapper').length; - indicator.textContent = `Scroll or click to show ${count} examples`; - icon.classList.replace('fa-chevron-up', 'fa-chevron-down'); - } -}; - -// Add lazy loading initialization -function initLazyLoading(container) { - const lazyElements = container.querySelectorAll('.lazy'); - - const lazyLoad = (element) => { - if (element.tagName.toLowerCase() === 'video') { - element.src = element.dataset.src; - element.querySelector('source').src = element.dataset.src; - element.load(); - } else { - element.src = element.dataset.src; - } - element.classList.remove('lazy'); - }; - - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - lazyLoad(entry.target); - observer.unobserve(entry.target); - } - }); - }); - - lazyElements.forEach(element => observer.observe(element)); -} - -export function setupShowcaseScroll() { - // Change from modal-content to window/document level - document.addEventListener('wheel', (event) => { - const modalContent = document.querySelector('.modal-content'); - if (!modalContent) return; - - const showcase = modalContent.querySelector('.showcase-section'); - if (!showcase) return; - - const carousel = showcase.querySelector('.carousel'); - const scrollIndicator = showcase.querySelector('.scroll-indicator'); - - if (carousel?.classList.contains('collapsed') && event.deltaY > 0) { - const isNearBottom = modalContent.scrollHeight - modalContent.scrollTop - modalContent.clientHeight < 100; - - if (isNearBottom) { - toggleShowcase(scrollIndicator); - event.preventDefault(); - } - } - }, { passive: false }); // Add passive: false option here - - // Keep the existing scroll tracking code - const modalContent = document.querySelector('.modal-content'); - if (modalContent) { - modalContent.addEventListener('scroll', () => { - const backToTopBtn = modalContent.querySelector('.back-to-top'); - if (backToTopBtn) { - if (modalContent.scrollTop > 300) { - backToTopBtn.classList.add('visible'); - } else { - backToTopBtn.classList.remove('visible'); - } - } - }); - } -} - -export function scrollToTop(button) { - const modalContent = button.closest('.modal-content'); - if (modalContent) { - modalContent.scrollTo({ - top: 0, - behavior: 'smooth' - }); - } -} - -function parsePresets(usageTips) { - if (!usageTips || usageTips === 'Save usage tips here..') return {}; - try { - return JSON.parse(usageTips); - } catch { - return {}; - } -} - -function renderPresetTags(presets) { - return Object.entries(presets).map(([key, value]) => ` -
- ${formatPresetKey(key)}: ${value} - -
- `).join(''); -} - -function formatPresetKey(key) { - return key.split('_').map(word => - word.charAt(0).toUpperCase() + word.slice(1) - ).join(' '); -} - -window.removePreset = async function(key) { - const filePath = document.querySelector('.modal-content') - .querySelector('.file-path').textContent + - document.querySelector('.modal-content') - .querySelector('#file-name').textContent + '.safetensors'; - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - const currentPresets = parsePresets(loraCard.dataset.usage_tips); - - delete currentPresets[key]; - const newPresetsJson = JSON.stringify(currentPresets); - - await saveModelMetadata(filePath, { - usage_tips: newPresetsJson - }); - - loraCard.dataset.usage_tips = newPresetsJson; - document.querySelector('.preset-tags').innerHTML = renderPresetTags(currentPresets); -}; - -// 添加文件大小格式化函数 -function formatFileSize(bytes) { - console.log('formatFileSize: ', bytes); - if (!bytes) return 'N/A'; - const units = ['B', 'KB', 'MB', 'GB']; - let size = bytes; - let unitIndex = 0; - - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; - } - - return `${size.toFixed(1)} ${units[unitIndex]}`; } \ No newline at end of file diff --git a/static/js/components/LoraModal.js b/static/js/components/LoraModal.js new file mode 100644 index 00000000..5e8d31c6 --- /dev/null +++ b/static/js/components/LoraModal.js @@ -0,0 +1,471 @@ +import { showToast } from '../utils/uiHelpers.js'; +import { state } from '../state/index.js'; + +export function showLoraModal(lora) { + const escapedWords = lora.civitai?.trainedWords?.length ? + lora.civitai.trainedWords.map(word => word.replace(/'/g, '\\\'')) : []; + + const content = ` + + `; + + modalManager.showModal('loraModal', content); + setupEditableFields(); + setupShowcaseScroll(); +} + +// 添加复制文件名的函数 +window.copyFileName = async function(fileName) { + try { + await navigator.clipboard.writeText(fileName); + showToast('File name copied', 'success'); + } catch (err) { + console.error('Copy failed:', err); + showToast('Copy failed', 'error'); + } +}; + +function setupEditableFields() { + const editableFields = document.querySelectorAll('.editable-field [contenteditable]'); + + editableFields.forEach(field => { + field.addEventListener('focus', function() { + if (this.textContent === 'Add your notes here...' || + this.textContent === 'Save usage tips here..') { + this.textContent = ''; + } + }); + + field.addEventListener('blur', function() { + if (this.textContent.trim() === '') { + this.textContent = this.classList.contains('usage-tips-content') + ? 'Save usage tips here..' + : 'Add your notes here...'; + } + }); + }); + + const presetSelector = document.getElementById('preset-selector'); + const presetValue = document.getElementById('preset-value'); + const addPresetBtn = document.querySelector('.add-preset-btn'); + const presetTags = document.querySelector('.preset-tags'); + + presetSelector.addEventListener('change', function() { + const selected = this.value; + if (selected) { + presetValue.style.display = 'inline-block'; + presetValue.min = selected.includes('strength') ? 0 : 1; + presetValue.max = selected.includes('strength') ? 1 : 12; + presetValue.step = selected.includes('strength') ? 0.01 : 1; + if (selected === 'clip_skip') { + presetValue.type = 'number'; + presetValue.step = 1; + } + // Add auto-focus + setTimeout(() => presetValue.focus(), 0); + } else { + presetValue.style.display = 'none'; + } + }); + + addPresetBtn.addEventListener('click', async function() { + const key = presetSelector.value; + const value = presetValue.value; + + if (!key || !value) return; + + const filePath = document.querySelector('.modal-content') + .querySelector('.file-path').textContent + + document.querySelector('.modal-content') + .querySelector('#file-name').textContent + '.safetensors'; + + const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); + const currentPresets = parsePresets(loraCard.dataset.usage_tips); + + currentPresets[key] = parseFloat(value); + const newPresetsJson = JSON.stringify(currentPresets); + + await saveModelMetadata(filePath, { + usage_tips: newPresetsJson + }); + + loraCard.dataset.usage_tips = newPresetsJson; + presetTags.innerHTML = renderPresetTags(currentPresets); + + presetSelector.value = ''; + presetValue.value = ''; + presetValue.style.display = 'none'; + }); + + // Add keydown event listeners for notes + const notesContent = document.querySelector('.notes-content'); + if (notesContent) { + notesContent.addEventListener('keydown', async function(e) { + if (e.key === 'Enter') { + if (e.shiftKey) { + // Allow shift+enter for new line + return; + } + e.preventDefault(); + const filePath = document.querySelector('.modal-content') + .querySelector('.file-path').textContent + + document.querySelector('.modal-content') + .querySelector('#file-name').textContent + '.safetensors'; + await saveNotes(filePath); + } + }); + } + + // Add keydown event for preset value + presetValue.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + addPresetBtn.click(); + } + }); +} + +window.saveNotes = async function(filePath) { + const content = document.querySelector('.notes-content').textContent; + try { + await saveModelMetadata(filePath, { notes: content }); + + // Update the corresponding lora card's dataset + const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); + if (loraCard) { + loraCard.dataset.notes = content; + } + + showToast('Notes saved successfully', 'success'); + } catch (error) { + showToast('Failed to save notes', 'error'); + } +}; + +async function saveModelMetadata(filePath, data) { + const response = await fetch('/loras/api/save-metadata', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file_path: filePath, + ...data + }) + }); + + if (!response.ok) { + throw new Error('Failed to save metadata'); + } +} + +function renderTriggerWords(words) { + if (!words.length) return ` +
+ + No trigger word needed +
+ `; + + return ` +
+ +
+ ${words.map(word => ` +
+ ${word} + + + +
+ `).join('')} +
+
+ `; +} + +function renderShowcaseImages(images) { + if (!images?.length) return ''; + + return ` +
+
+ + Scroll or click to show ${images.length} examples +
+ + +
+ `; +} + +export function toggleShowcase(element) { + const carousel = element.nextElementSibling; + const isCollapsed = carousel.classList.contains('collapsed'); + const indicator = element.querySelector('span'); + const icon = element.querySelector('i'); + + carousel.classList.toggle('collapsed'); + + if (isCollapsed) { + const count = carousel.querySelectorAll('.media-wrapper').length; + indicator.textContent = `Scroll or click to hide examples`; + icon.classList.replace('fa-chevron-down', 'fa-chevron-up'); + initLazyLoading(carousel); + } else { + const count = carousel.querySelectorAll('.media-wrapper').length; + indicator.textContent = `Scroll or click to show ${count} examples`; + icon.classList.replace('fa-chevron-up', 'fa-chevron-down'); + } +} + +// Add lazy loading initialization +function initLazyLoading(container) { + const lazyElements = container.querySelectorAll('.lazy'); + + const lazyLoad = (element) => { + if (element.tagName.toLowerCase() === 'video') { + element.src = element.dataset.src; + element.querySelector('source').src = element.dataset.src; + element.load(); + } else { + element.src = element.dataset.src; + } + element.classList.remove('lazy'); + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + lazyLoad(entry.target); + observer.unobserve(entry.target); + } + }); + }); + + lazyElements.forEach(element => observer.observe(element)); +} + +export function setupShowcaseScroll() { + // Change from modal-content to window/document level + document.addEventListener('wheel', (event) => { + const modalContent = document.querySelector('.modal-content'); + if (!modalContent) return; + + const showcase = modalContent.querySelector('.showcase-section'); + if (!showcase) return; + + const carousel = showcase.querySelector('.carousel'); + const scrollIndicator = showcase.querySelector('.scroll-indicator'); + + if (carousel?.classList.contains('collapsed') && event.deltaY > 0) { + const isNearBottom = modalContent.scrollHeight - modalContent.scrollTop - modalContent.clientHeight < 100; + + if (isNearBottom) { + toggleShowcase(scrollIndicator); + event.preventDefault(); + } + } + }, { passive: false }); // Add passive: false option here + + // Keep the existing scroll tracking code + const modalContent = document.querySelector('.modal-content'); + if (modalContent) { + modalContent.addEventListener('scroll', () => { + const backToTopBtn = modalContent.querySelector('.back-to-top'); + if (backToTopBtn) { + if (modalContent.scrollTop > 300) { + backToTopBtn.classList.add('visible'); + } else { + backToTopBtn.classList.remove('visible'); + } + } + }); + } +} + +export function scrollToTop(button) { + const modalContent = button.closest('.modal-content'); + if (modalContent) { + modalContent.scrollTo({ + top: 0, + behavior: 'smooth' + }); + } +} + +function parsePresets(usageTips) { + if (!usageTips || usageTips === 'Save usage tips here..') return {}; + try { + return JSON.parse(usageTips); + } catch { + return {}; + } +} + +function renderPresetTags(presets) { + return Object.entries(presets).map(([key, value]) => ` +
+ ${formatPresetKey(key)}: ${value} + +
+ `).join(''); +} + +function formatPresetKey(key) { + return key.split('_').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' '); +} + +window.removePreset = async function(key) { + const filePath = document.querySelector('.modal-content') + .querySelector('.file-path').textContent + + document.querySelector('.modal-content') + .querySelector('#file-name').textContent + '.safetensors'; + const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); + const currentPresets = parsePresets(loraCard.dataset.usage_tips); + + delete currentPresets[key]; + const newPresetsJson = JSON.stringify(currentPresets); + + await saveModelMetadata(filePath, { + usage_tips: newPresetsJson + }); + + loraCard.dataset.usage_tips = newPresetsJson; + document.querySelector('.preset-tags').innerHTML = renderPresetTags(currentPresets); +}; + +// 添加文件大小格式化函数 +function formatFileSize(bytes) { + if (!bytes) return 'N/A'; + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(1)} ${units[unitIndex]}`; +} \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index de4189a8..cb674c68 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -3,7 +3,8 @@ import { LoadingManager } from './managers/LoadingManager.js'; import { modalManager } from './managers/ModalManager.js'; import { updateService } from './managers/UpdateService.js'; import { state } from './state/index.js'; -import { showLoraModal, toggleShowcase, scrollToTop } from './components/LoraCard.js'; +import { showLoraModal } from './components/LoraModal.js'; +import { toggleShowcase, scrollToTop } from './components/LoraModal.js'; import { loadMoreLoras, fetchCivitai, deleteModel, replacePreview, resetAndReload, refreshLoras } from './api/loraApi.js'; import { showToast,