From fd593bb61d2c6b1f9308782982884a1fc008861b Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Mon, 5 May 2025 20:50:32 +0800 Subject: [PATCH] feat: add source URL functionality to recipe modal, including dynamic display and editing options --- py/routes/recipe_routes.py | 4 +- static/css/components/recipe-modal.css | 163 +++++++++++++++++++++++++ static/js/components/RecipeModal.js | 93 ++++++++++++++ templates/components/recipe_modal.html | 1 + 4 files changed, 259 insertions(+), 2 deletions(-) diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index 38071d7d..dc649eba 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -1169,9 +1169,9 @@ class RecipeRoutes: data = await request.json() # Validate required fields - if 'title' not in data and 'tags' not in data: + if 'title' not in data and 'tags' not in data and 'source_path' not in data: return web.json_response({ - "error": "At least one field to update must be provided (title or tags)" + "error": "At least one field to update must be provided (title or tags or source_path)" }, status=400) # Use the recipe scanner's update method diff --git a/static/css/components/recipe-modal.css b/static/css/components/recipe-modal.css index 399ce01d..7249d151 100644 --- a/static/css/components/recipe-modal.css +++ b/static/css/components/recipe-modal.css @@ -229,8 +229,10 @@ background: var(--lora-surface); border: 1px solid var(--border-color); display: flex; + flex-direction: column; align-items: center; justify-content: center; + position: relative; } .recipe-preview-container img, @@ -246,6 +248,167 @@ object-fit: contain; } +/* Source URL container */ +.source-url-container { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.5); + padding: 8px 12px; + display: flex; + justify-content: space-between; + align-items: center; + transition: transform 0.3s ease; + transform: translateY(100%); +} + +.recipe-preview-container:hover .source-url-container { + transform: translateY(0); +} + +.source-url-container.active { + transform: translateY(0); +} + +.source-url-content { + display: flex; + align-items: center; + color: #fff; + flex: 1; + overflow: hidden; + font-size: 0.85em; +} + +.source-url-icon { + margin-right: 8px; + flex-shrink: 0; +} + +.source-url-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + flex: 1; +} + +.source-url-edit-btn { + background: none; + border: none; + color: #fff; + cursor: pointer; + padding: 4px; + margin-left: 8px; + border-radius: var(--border-radius-xs); + opacity: 0.7; + transition: opacity 0.2s ease; + flex-shrink: 0; +} + +.source-url-edit-btn:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} + +/* Source URL editor */ +.source-url-editor { + display: none; + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: var(--bg-color); + border-top: 1px solid var(--border-color); + padding: 12px; + flex-direction: column; + gap: 10px; + z-index: 5; +} + +.source-url-editor.active { + display: flex; +} + +.source-url-input { + width: 100%; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + background: var(--bg-color); + color: var(--text-color); + font-size: 0.9em; +} + +.source-url-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.source-url-cancel-btn, +.source-url-save-btn { + padding: 6px 12px; + border-radius: var(--border-radius-xs); + font-size: 0.85em; + cursor: pointer; + border: none; + transition: all 0.2s; +} + +.source-url-cancel-btn { + background: var(--bg-color); + color: var(--text-color); + border: 1px solid var(--border-color); +} + +.source-url-save-btn { + background: var(--lora-accent); + color: white; +} + +.source-url-cancel-btn:hover { + background: var(--lora-surface); +} + +.source-url-save-btn:hover { + background: color-mix(in oklch, var(--lora-accent), black 10%); +} + +/* Info tooltip icon for source URL */ +.source-url-info { + margin-left: 8px; + opacity: 0.7; + cursor: help; + position: relative; +} + +.source-url-info:hover { + opacity: 1; +} + +.source-url-tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: var(--card-bg); + color: var(--text-color); + padding: 8px 12px; + border-radius: var(--border-radius-xs); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + white-space: nowrap; + font-size: 0.85em; + font-weight: normal; + margin-bottom: 8px; + z-index: 10; + display: none; +} + +.source-url-info:hover .source-url-tooltip { + display: block; +} + /* Generation Parameters */ .recipe-gen-params { height: 360px; diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index e05e43bc..399afbbb 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -245,6 +245,49 @@ class RecipeModal { imgElement.alt = recipe.title || 'Recipe Preview'; mediaContainer.appendChild(imgElement); } + + // Add source URL container if the recipe has a source_path + const sourceUrlContainer = document.createElement('div'); + sourceUrlContainer.className = 'source-url-container'; + const hasSourceUrl = recipe.source_path && recipe.source_path.trim().length > 0; + const sourceUrl = hasSourceUrl ? recipe.source_path : ''; + const isValidUrl = hasSourceUrl && (sourceUrl.startsWith('http://') || sourceUrl.startsWith('https://')); + + sourceUrlContainer.innerHTML = ` +
+ + ${ + hasSourceUrl ? sourceUrl : 'No source URL' + } + + + Source of the original image (e.g., Civitai post) + +
+ + `; + + // Add source URL editor + const sourceUrlEditor = document.createElement('div'); + sourceUrlEditor.className = 'source-url-editor'; + sourceUrlEditor.innerHTML = ` + +
+ + +
+ `; + + // Append both containers to the media container + mediaContainer.appendChild(sourceUrlContainer); + mediaContainer.appendChild(sourceUrlEditor); + + // Set up event listeners for source URL functionality + setTimeout(() => { + this.setupSourceUrlHandlers(); + }, 50); } // Set generation parameters @@ -1067,6 +1110,56 @@ class RecipeModal { }); }); } + + // New method to set up source URL handlers + setupSourceUrlHandlers() { + const sourceUrlContainer = document.querySelector('.source-url-container'); + const sourceUrlEditor = document.querySelector('.source-url-editor'); + const sourceUrlText = sourceUrlContainer.querySelector('.source-url-text'); + const sourceUrlEditBtn = sourceUrlContainer.querySelector('.source-url-edit-btn'); + const sourceUrlCancelBtn = sourceUrlEditor.querySelector('.source-url-cancel-btn'); + const sourceUrlSaveBtn = sourceUrlEditor.querySelector('.source-url-save-btn'); + const sourceUrlInput = sourceUrlEditor.querySelector('.source-url-input'); + + // Show editor on edit button click + sourceUrlEditBtn.addEventListener('click', () => { + sourceUrlContainer.classList.add('hide'); + sourceUrlEditor.classList.add('active'); + sourceUrlInput.focus(); + }); + + // Cancel editing + sourceUrlCancelBtn.addEventListener('click', () => { + sourceUrlEditor.classList.remove('active'); + sourceUrlContainer.classList.remove('hide'); + sourceUrlInput.value = this.currentRecipe.source_path || ''; + }); + + // Save new source URL + sourceUrlSaveBtn.addEventListener('click', () => { + const newSourceUrl = sourceUrlInput.value.trim(); + if (newSourceUrl && newSourceUrl !== this.currentRecipe.source_path) { + // Update source URL in the UI + sourceUrlText.textContent = newSourceUrl; + sourceUrlText.title = newSourceUrl.startsWith('http://') || newSourceUrl.startsWith('https://') ? 'Click to open source URL' : 'No valid URL'; + + // Update the recipe on the server + this.updateRecipeMetadata({ source_path: newSourceUrl }); + } + + // Hide editor + sourceUrlEditor.classList.remove('active'); + sourceUrlContainer.classList.remove('hide'); + }); + + // Open source URL in a new tab if it's valid + sourceUrlText.addEventListener('click', () => { + const url = sourceUrlText.textContent.trim(); + if (url.startsWith('http://') || url.startsWith('https://')) { + window.open(url, '_blank'); + } + }); + } } export { RecipeModal }; \ No newline at end of file diff --git a/templates/components/recipe_modal.html b/templates/components/recipe_modal.html index 79d0ce82..fb1b74cf 100644 --- a/templates/components/recipe_modal.html +++ b/templates/components/recipe_modal.html @@ -18,6 +18,7 @@
Recipe Preview +