From 85f987d15cef5f47380483b0ab669ab0feb03d70 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Tue, 22 Apr 2025 09:33:05 +0800 Subject: [PATCH] feat: Centralize clipboard functionality with copyToClipboard utility across components --- static/js/components/CheckpointCard.js | 18 +-------- static/js/components/LoraCard.js | 23 +---------- static/js/components/RecipeCard.js | 9 ++--- static/js/components/RecipeModal.js | 14 ++----- .../checkpointModal/ShowcaseView.js | 5 +-- static/js/components/loraModal/RecipeTab.js | 7 +--- .../js/components/loraModal/ShowcaseView.js | 5 +-- .../js/components/loraModal/TriggerWords.js | 20 +--------- static/js/components/loraModal/index.js | 6 +-- static/js/managers/BulkManager.js | 10 +---- static/js/utils/uiHelpers.js | 40 ++++++++++++++++--- 11 files changed, 57 insertions(+), 100 deletions(-) diff --git a/static/js/components/CheckpointCard.js b/static/js/components/CheckpointCard.js index 4984d2ca..96df010a 100644 --- a/static/js/components/CheckpointCard.js +++ b/static/js/components/CheckpointCard.js @@ -1,4 +1,4 @@ -import { showToast } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { showCheckpointModal } from './checkpointModal/index.js'; import { NSFW_LEVELS } from '../utils/constants.js'; @@ -204,21 +204,7 @@ export function createCheckpointCard(checkpoint) { const checkpointName = card.dataset.file_name; try { - // Modern clipboard API - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(checkpointName); - } else { - // Fallback for older browsers - const textarea = document.createElement('textarea'); - textarea.value = checkpointName; - textarea.style.position = 'absolute'; - textarea.style.left = '-99999px'; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); - } - showToast('Checkpoint name copied', 'success'); + await copyToClipboard(checkpointName, 'Checkpoint name copied'); } catch (err) { console.error('Copy failed:', err); showToast('Copy failed', 'error'); diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js index fa5e8a0b..fb97aadb 100644 --- a/static/js/components/LoraCard.js +++ b/static/js/components/LoraCard.js @@ -1,4 +1,4 @@ -import { showToast, openCivitai } from '../utils/uiHelpers.js'; +import { showToast, openCivitai, copyToClipboard } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { showLoraModal } from './loraModal/index.js'; import { bulkManager } from '../managers/BulkManager.js'; @@ -205,26 +205,7 @@ export function createLoraCard(lora) { const strength = usageTips.strength || 1; const loraSyntax = ``; - try { - // Modern clipboard API - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(loraSyntax); - } else { - // Fallback for older browsers - const textarea = document.createElement('textarea'); - textarea.value = loraSyntax; - textarea.style.position = 'absolute'; - textarea.style.left = '-99999px'; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); - } - showToast('LoRA syntax copied', 'success'); - } catch (err) { - console.error('Copy failed:', err); - showToast('Copy failed', 'error'); - } + await copyToClipboard(loraSyntax, 'LoRA syntax copied'); }); // Civitai button click event diff --git a/static/js/components/RecipeCard.js b/static/js/components/RecipeCard.js index d4285b21..c87d52bb 100644 --- a/static/js/components/RecipeCard.js +++ b/static/js/components/RecipeCard.js @@ -1,5 +1,5 @@ // Recipe Card Component -import { showToast } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; import { modalManager } from '../managers/ModalManager.js'; class RecipeCard { @@ -109,14 +109,11 @@ class RecipeCard { .then(response => response.json()) .then(data => { if (data.success && data.syntax) { - return navigator.clipboard.writeText(data.syntax); + return copyToClipboard(data.syntax, 'Recipe syntax copied to clipboard'); } else { throw new Error(data.error || 'No syntax returned'); } }) - .then(() => { - showToast('Recipe syntax copied to clipboard', 'success'); - }) .catch(err => { console.error('Failed to copy: ', err); showToast('Failed to copy recipe syntax', 'error'); @@ -279,4 +276,4 @@ class RecipeCard { } } -export { RecipeCard }; \ No newline at end of file +export { RecipeCard }; \ No newline at end of file diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index ec09ec0c..5c81b242 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -1,5 +1,5 @@ // Recipe Modal Component -import { showToast } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { setSessionItem, removeSessionItem } from '../utils/storageHelpers.js'; @@ -747,9 +747,8 @@ class RecipeModal { const data = await response.json(); if (data.success && data.syntax) { - // Copy to clipboard - await navigator.clipboard.writeText(data.syntax); - showToast('Recipe syntax copied to clipboard', 'success'); + // Use the centralized copyToClipboard utility function + await copyToClipboard(data.syntax, 'Recipe syntax copied to clipboard'); } else { throw new Error(data.error || 'No syntax returned from server'); } @@ -761,12 +760,7 @@ class RecipeModal { // Helper method to copy text to clipboard copyToClipboard(text, successMessage) { - navigator.clipboard.writeText(text).then(() => { - showToast(successMessage, 'success'); - }).catch(err => { - console.error('Failed to copy text: ', err); - showToast('Failed to copy text', 'error'); - }); + copyToClipboard(text, successMessage); } // Add new method to handle downloading missing LoRAs diff --git a/static/js/components/checkpointModal/ShowcaseView.js b/static/js/components/checkpointModal/ShowcaseView.js index d9843fc3..0ee80079 100644 --- a/static/js/components/checkpointModal/ShowcaseView.js +++ b/static/js/components/checkpointModal/ShowcaseView.js @@ -2,7 +2,7 @@ * ShowcaseView.js * Handles showcase content (images, videos) display for checkpoint modal */ -import { showToast } from '../../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { state } from '../../state/index.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; @@ -307,8 +307,7 @@ function initMetadataPanelHandlers(container) { if (!promptElement) return; try { - await navigator.clipboard.writeText(promptElement.textContent); - showToast('Prompt copied to clipboard', 'success'); + await copyToClipboard(promptElement.textContent, 'Prompt copied to clipboard'); } catch (err) { console.error('Copy failed:', err); showToast('Copy failed', 'error'); diff --git a/static/js/components/loraModal/RecipeTab.js b/static/js/components/loraModal/RecipeTab.js index 264d24e5..412b4532 100644 --- a/static/js/components/loraModal/RecipeTab.js +++ b/static/js/components/loraModal/RecipeTab.js @@ -1,7 +1,7 @@ /** * RecipeTab - Handles the recipes tab in the Lora Modal */ -import { showToast } from '../../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; /** @@ -172,14 +172,11 @@ function copyRecipeSyntax(recipeId) { .then(response => response.json()) .then(data => { if (data.success && data.syntax) { - return navigator.clipboard.writeText(data.syntax); + return copyToClipboard(data.syntax, 'Recipe syntax copied to clipboard'); } else { throw new Error(data.error || 'No syntax returned'); } }) - .then(() => { - showToast('Recipe syntax copied to clipboard', 'success'); - }) .catch(err => { console.error('Failed to copy: ', err); showToast('Failed to copy recipe syntax', 'error'); diff --git a/static/js/components/loraModal/ShowcaseView.js b/static/js/components/loraModal/ShowcaseView.js index 2e2858e9..9bd7ee3d 100644 --- a/static/js/components/loraModal/ShowcaseView.js +++ b/static/js/components/loraModal/ShowcaseView.js @@ -2,7 +2,7 @@ * ShowcaseView.js * 处理LoRA模型展示内容(图片、视频)的功能模块 */ -import { showToast } from '../../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { state } from '../../state/index.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; @@ -311,8 +311,7 @@ function initMetadataPanelHandlers(container) { if (!promptElement) return; try { - await navigator.clipboard.writeText(promptElement.textContent); - showToast('Prompt copied to clipboard', 'success'); + await copyToClipboard(promptElement.textContent, 'Prompt copied to clipboard'); } catch (err) { console.error('Copy failed:', err); showToast('Copy failed', 'error'); diff --git a/static/js/components/loraModal/TriggerWords.js b/static/js/components/loraModal/TriggerWords.js index 537bd37a..e80c9e39 100644 --- a/static/js/components/loraModal/TriggerWords.js +++ b/static/js/components/loraModal/TriggerWords.js @@ -2,7 +2,7 @@ * TriggerWords.js * 处理LoRA模型触发词相关的功能模块 */ -import { showToast } from '../../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { saveModelMetadata } from './ModelMetadata.js'; /** @@ -336,23 +336,7 @@ async function saveTriggerWords() { */ window.copyTriggerWord = async function(word) { try { - // Modern clipboard API - with fallback for non-secure contexts - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(word); - } else { - // Fallback for older browsers or non-secure contexts - const textarea = document.createElement('textarea'); - textarea.value = word; - textarea.style.position = 'absolute'; - textarea.style.left = '-99999px'; - document.body.appendChild(textarea); - textarea.select(); - const success = document.execCommand('copy'); - document.body.removeChild(textarea); - - if (!success) throw new Error('Copy command failed'); - } - showToast('Trigger word copied', 'success'); + await copyToClipboard(word, 'Trigger word copied'); } catch (err) { console.error('Copy failed:', err); showToast('Copy failed', 'error'); diff --git a/static/js/components/loraModal/index.js b/static/js/components/loraModal/index.js index 017a1b2a..69e71675 100644 --- a/static/js/components/loraModal/index.js +++ b/static/js/components/loraModal/index.js @@ -3,8 +3,7 @@ * * 将原始的LoraModal.js拆分成多个功能模块后的主入口文件 */ -import { showToast } from '../../utils/uiHelpers.js'; -import { state } from '../../state/index.js'; +import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { modalManager } from '../../managers/ModalManager.js'; import { renderShowcaseContent, toggleShowcase, setupShowcaseScroll, scrollToTop } from './ShowcaseView.js'; import { setupTabSwitching, loadModelDescription } from './ModelDescription.js'; @@ -174,8 +173,7 @@ export function showLoraModal(lora) { // Copy file name function window.copyFileName = async function(fileName) { try { - await navigator.clipboard.writeText(fileName); - showToast('File name copied', 'success'); + await copyToClipboard(fileName, 'File name copied'); } catch (err) { console.error('Copy failed:', err); showToast('Copy failed', 'error'); diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 76fa5043..7826841d 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -1,5 +1,5 @@ import { state } from '../state/index.js'; -import { showToast } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; import { updateCardsForBulkMode } from '../components/LoraCard.js'; export class BulkManager { @@ -205,13 +205,7 @@ export class BulkManager { return; } - try { - await navigator.clipboard.writeText(loraSyntaxes.join(', ')); - showToast(`Copied ${loraSyntaxes.length} LoRA syntaxes to clipboard`, 'success'); - } catch (err) { - console.error('Copy failed:', err); - showToast('Copy failed', 'error'); - } + await copyToClipboard(loraSyntaxes.join(', '), `Copied ${loraSyntaxes.length} LoRA syntaxes to clipboard`); } // Create and show the thumbnail strip of selected LoRAs diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 9c2a6dd1..cd2edde0 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -2,6 +2,40 @@ import { state } from '../state/index.js'; import { resetAndReload } from '../api/loraApi.js'; import { getStorageItem, setStorageItem } from './storageHelpers.js'; +/** + * Utility function to copy text to clipboard with fallback for older browsers + * @param {string} text - The text to copy to clipboard + * @param {string} successMessage - Optional success message to show in toast + * @returns {Promise} - Promise that resolves to true if copy was successful + */ +export async function copyToClipboard(text, successMessage = 'Copied to clipboard') { + try { + // Modern clipboard API + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + } else { + // Fallback for older browsers + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'absolute'; + textarea.style.left = '-99999px'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + } + + if (successMessage) { + showToast(successMessage, 'success'); + } + return true; + } catch (err) { + console.error('Copy failed:', err); + showToast('Copy failed', 'error'); + return false; + } +} + export function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; @@ -108,12 +142,6 @@ export function toggleFolder(tag) { resetAndReload(); } -export function copyTriggerWord(word) { - navigator.clipboard.writeText(word).then(() => { - showToast('Trigger word copied', 'success'); - }); -} - function filterByFolder(folderPath) { document.querySelectorAll('.lora-card').forEach(card => { card.style.display = card.dataset.folder === folderPath ? '' : 'none';