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 = ` +