From ac9a8d145f55a0002195cc8632c90f7586467198 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 5 Feb 2026 21:23:59 -0500 Subject: [PATCH] DRYing the project --- .claude/settings.local.json | 4 +- app/static/js/bulk-select.js | 49 ++----------- app/static/js/gallery-infinite.js | 27 ++----- app/static/js/showcase.js | 43 ++---------- app/static/js/tag-utils.js | 112 ++++++++++++++++++++++++++++++ app/static/js/view-modal.js | 38 ++-------- app/static/style.css | 1 - app/templates/gallery.html | 1 + app/templates/showcase.html | 29 +------- dockerfile | 27 ------- summary.md | 37 +++++++--- 11 files changed, 168 insertions(+), 200 deletions(-) create mode 100644 app/static/js/tag-utils.js delete mode 100644 dockerfile diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ada7a03..9d85156 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,9 @@ "permissions": { "allow": [ "Bash(python:*)", - "Bash(git mv:*)" + "Bash(git mv:*)", + "Bash(grep:*)", + "Bash(wc:*)" ] } } diff --git a/app/static/js/bulk-select.js b/app/static/js/bulk-select.js index ca6e08f..30a15dc 100644 --- a/app/static/js/bulk-select.js +++ b/app/static/js/bulk-select.js @@ -5,6 +5,9 @@ (function() { 'use strict'; + // Use shared TagUtils module + const { getTagIcon, getTagDisplayName, getTagDisplayNameForOverlay, escapeHtml, createTagChipHtml } = window.TagUtils || {}; + const state = { isSelectMode: false, selectedIds: new Set(), @@ -263,7 +266,7 @@ dom.commonTags.innerHTML = tags.map(t => { const icon = getTagIcon(t.kind); - const display = t.name.includes(':') ? t.name.split(':')[1] : t.name; + const display = getTagDisplayName(t.name, t.kind); const escapedName = escapeHtml(t.name); const escapedDisplay = escapeHtml(display); return ` @@ -471,7 +474,7 @@ dom.autocomplete.innerHTML = tags.map((t, i) => { const icon = getTagIcon(t.kind); - const displayName = t.name.includes(':') ? t.name.split(':')[1] : t.name; + const displayName = getTagDisplayName(t.name, t.kind); return `
${icon} ${escapeHtml(displayName)} ${t.kind || 'user'} @@ -491,41 +494,9 @@ autocompleteSelectedIndex = index; } - // --------------------------- - // Helpers - // --------------------------- - function getTagIcon(kind) { - const icons = { - artist: '🎨', - archive: '🗜️', - character: '👤', - series: '📺', - fandom: '🎭', - rating: '⚠️', - source: '🌐', - post: '📌' - }; - return icons[kind] || '#'; - } - - function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; - } - // --------------------------- // Tag Refresh for Gallery Items // --------------------------- - function getTagDisplayName(name, kind) { - // Strip prefix for known prefixed kinds - if (['artist', 'character', 'series', 'fandom', 'rating'].includes(kind)) { - return name.includes(':') ? name.split(':', 2)[1] : name; - } - // Regular tags get a # prefix - return '#' + name; - } - async function refreshItemTags(imageId) { // Find the gallery item const item = document.querySelector(`.img-clickable[data-id="${imageId}"]`); @@ -555,14 +526,8 @@ item.appendChild(overlay); } - // Render tags with icons - overlay.innerHTML = visibleTags.map(t => { - const displayName = getTagDisplayName(t.name, t.kind); - const icon = getTagIcon(t.kind); - const encodedName = encodeURIComponent(t.name); - const prefix = icon !== '#' ? icon + ' ' : '#'; - return `${prefix}${escapeHtml(displayName)}`; - }).join(''); + // Render tags with icons using shared utility + overlay.innerHTML = visibleTags.map(t => createTagChipHtml(t)).join(''); } catch (e) { console.error('Failed to refresh tags for image', imageId, e); diff --git a/app/static/js/gallery-infinite.js b/app/static/js/gallery-infinite.js index 0305ed3..e1d03d4 100644 --- a/app/static/js/gallery-infinite.js +++ b/app/static/js/gallery-infinite.js @@ -4,6 +4,9 @@ (function() { 'use strict'; + // Use shared TagUtils module + const { getTagIcon, getTagDisplayName, escapeHtml, createTagChipHtml } = window.TagUtils || {}; + // Configuration const CONFIG = { SCROLL_THRESHOLD: 1200, // px from bottom to trigger load @@ -187,21 +190,6 @@ return section; } - // Tag icon mapping (matches showcase.js and bulk-select.js) - function getTagIcon(kind) { - const icons = { - artist: '🎨', - archive: '🗜️', - character: '👤', - series: '📺', - fandom: '🎭', - rating: '⚠️', - source: '🌐', - post: '📌' - }; - return icons[kind] || '#'; - } - // Create a single image element function createImageElement(img) { const item = document.createElement('div'); @@ -211,15 +199,10 @@ item.dataset.type = img.is_video ? 'video' : 'image'; item.dataset.filename = img.filename; - // Build tag overlay HTML with icons + // Build tag overlay HTML with icons using shared utility let tagsHtml = ''; if (img.tags && img.tags.length > 0) { - const tagChips = img.tags.map(t => { - const displayName = t.name.includes(':') ? t.name.split(':')[1] : t.name; - const icon = getTagIcon(t.kind); - const prefix = icon !== '#' ? icon + ' ' : '#'; - return `${prefix}${displayName}`; - }).join(''); + const tagChips = img.tags.map(t => createTagChipHtml(t)).join(''); tagsHtml = `
${tagChips}
`; } diff --git a/app/static/js/showcase.js b/app/static/js/showcase.js index 0d7721c..79d5571 100644 --- a/app/static/js/showcase.js +++ b/app/static/js/showcase.js @@ -2,6 +2,11 @@ // JS-managed masonry with column distribution for infinite scroll (function() { + 'use strict'; + + // Use shared TagUtils module + const { formatTagHtml, createTagChipHtml } = window.TagUtils || {}; + const container = document.getElementById('showcaseGrid'); if (!container) return; @@ -156,15 +161,8 @@ if (visibleTags.length > 0) { const overlay = document.createElement('div'); overlay.className = 'tag-overlay'; - visibleTags.forEach(t => { - const chip = document.createElement('a'); - chip.className = 'tag-chip'; - chip.href = `/gallery?tag=${encodeURIComponent(t.name)}`; - chip.title = `Filter by ${t.name}`; - chip.onclick = (e) => e.stopPropagation(); - chip.innerHTML = formatTag(t); - overlay.appendChild(chip); - }); + // Use shared utility to create tag chips + overlay.innerHTML = visibleTags.map(t => createTagChipHtml(t)).join(''); item.appendChild(overlay); } @@ -274,33 +272,6 @@ } } - function formatTag(tag) { - const escape = (s) => { - const div = document.createElement('div'); - div.textContent = s; - return div.innerHTML; - }; - - const icons = { - artist: '🎨', - archive: '🗜️', - character: '👤', - series: '📺', - fandom: '🎭', - rating: '⚠️', - source: '🌐', - post: '📌' - }; - - const icon = icons[tag.kind]; - if (icon) { - const label = tag.name.includes(':') ? tag.name.split(':', 2)[1] : tag.name; - return `${icon} ${escape(label)}`; - } else { - return `#${escape(tag.name)}`; - } - } - // Start init(); })(); diff --git a/app/static/js/tag-utils.js b/app/static/js/tag-utils.js new file mode 100644 index 0000000..3ff60ee --- /dev/null +++ b/app/static/js/tag-utils.js @@ -0,0 +1,112 @@ +// /app/static/js/tag-utils.js +// Shared tag utility functions used across gallery, modal, and bulk editor + +(function() { + 'use strict'; + + /** + * Get emoji icon for a tag kind + * @param {string} kind - Tag kind (artist, archive, character, etc.) + * @returns {string} Emoji icon or '#' for user tags + */ + function getTagIcon(kind) { + const icons = { + artist: '🎨', + archive: '🗜️', + character: '👤', + series: '📺', + fandom: '🎭', + rating: '⚠️', + source: '🌐', + post: '📌' + }; + return icons[kind] || '#'; + } + + /** + * Get display name for a tag, stripping the prefix if present + * @param {string} name - Full tag name (e.g., "artist:Name") + * @param {string} [kind] - Optional tag kind for context + * @returns {string} Display name without prefix + */ + function getTagDisplayName(name, kind) { + // For prefixed kinds, strip the prefix + if (kind && ['artist', 'character', 'series', 'fandom', 'rating'].includes(kind)) { + return name.includes(':') ? name.split(':', 2)[1] : name; + } + // If no kind specified but has colon, assume it's prefixed + if (!kind && name.includes(':')) { + return name.split(':', 2)[1]; + } + return name; + } + + /** + * Get display name for tag overlay (used in gallery items) + * Returns name with # prefix for unprefixed user tags + * @param {string} name - Full tag name + * @param {string} kind - Tag kind + * @returns {string} Display name for overlay + */ + function getTagDisplayNameForOverlay(name, kind) { + if (['artist', 'character', 'series', 'fandom', 'rating'].includes(kind)) { + return name.includes(':') ? name.split(':', 2)[1] : name; + } + return '#' + name; + } + + /** + * Escape HTML special characters + * @param {string} str - String to escape + * @returns {string} HTML-escaped string + */ + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + /** + * Format a tag for display with icon and name + * @param {Object} tag - Tag object with name and kind + * @returns {string} Formatted HTML string + */ + function formatTagHtml(tag) { + const icon = getTagIcon(tag.kind); + const displayName = getTagDisplayName(tag.name, tag.kind); + const escaped = escapeHtml(displayName); + + if (icon !== '#') { + return `${icon} ${escaped}`; + } + return `#${escaped}`; + } + + /** + * Create tag chip HTML for gallery overlays + * @param {Object} tag - Tag object with name and kind + * @param {boolean} [stopPropagation=true] - Add onclick to stop propagation + * @returns {string} HTML string for tag chip link + */ + function createTagChipHtml(tag, stopPropagation = true) { + const icon = getTagIcon(tag.kind); + const displayName = getTagDisplayNameForOverlay(tag.name, tag.kind); + const encodedName = encodeURIComponent(tag.name); + const escapedTitle = escapeHtml(tag.name); + const escapedDisplay = escapeHtml(displayName); + const prefix = icon !== '#' ? icon + ' ' : '#'; + const onclick = stopPropagation ? ' onclick="event.stopPropagation()"' : ''; + + return `${prefix}${escapedDisplay}`; + } + + // Export to window.TagUtils + window.TagUtils = { + getTagIcon, + getTagDisplayName, + getTagDisplayNameForOverlay, + escapeHtml, + formatTagHtml, + createTagChipHtml + }; +})(); diff --git a/app/static/js/view-modal.js b/app/static/js/view-modal.js index f73d661..5dda426 100644 --- a/app/static/js/view-modal.js +++ b/app/static/js/view-modal.js @@ -57,30 +57,15 @@ document.addEventListener('DOMContentLoaded', () => { // --------------------------- // Tag editor helpers // --------------------------- + // Use shared TagUtils module + const { getTagIcon, getTagDisplayName, getTagDisplayNameForOverlay, escapeHtml, createTagChipHtml } = window.TagUtils || {}; + function setEditorImageId(id) { if (tagEditor) tagEditor.dataset.imageId = id || ''; } function getEditorImageId() { return tagEditor ? tagEditor.dataset.imageId : ''; } - function getTagIcon(kind) { - const icons = { - artist: '🎨', - archive: '🗜️', - character: '👤', - series: '📺', - fandom: '🎭', - rating: '⚠️', - source: '🌐', - post: '📌' - }; - return icons[kind] || '#'; - } - - function getTagDisplayName(name) { - // Remove prefix if present (e.g., "artist:Name" -> "Name") - return name.includes(':') ? name.split(':', 2)[1] : name; - } function renderTags(tags) { if (!tagList) return; @@ -108,13 +93,6 @@ document.addEventListener('DOMContentLoaded', () => { // --------------------------- // Gallery item tag refresh (fallback if bulk-select.js not loaded) // --------------------------- - function getTagDisplayNameForOverlay(name, kind) { - if (['artist', 'character', 'series', 'fandom', 'rating'].includes(kind)) { - return name.includes(':') ? name.split(':', 2)[1] : name; - } - return '#' + name; - } - async function refreshItemTagsFallback(imageId) { const item = document.querySelector(`.img-clickable[data-id="${imageId}"]`); if (!item) return; @@ -138,15 +116,7 @@ document.addEventListener('DOMContentLoaded', () => { item.appendChild(overlay); } - overlay.innerHTML = visibleTags.map(t => { - const displayName = getTagDisplayNameForOverlay(t.name, t.kind); - const icon = getTagIcon(t.kind); - const encodedName = encodeURIComponent(t.name); - const escapedName = t.name.replace(/"/g, '"'); - const escapedDisplay = displayName.replace(//g, '>'); - const prefix = icon !== '#' ? icon + ' ' : '#'; - return `${prefix}${escapedDisplay}`; - }).join(''); + overlay.innerHTML = visibleTags.map(t => createTagChipHtml(t)).join(''); } catch (e) { console.error('Failed to refresh tags for image', imageId, e); } diff --git a/app/static/style.css b/app/static/style.css index d10ed0c..b6d07af 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -174,7 +174,6 @@ header { box-shadow: 0 4px 12px rgba(0,0,0,0.5); z-index: 1001; padding: 0.5rem 0; - margin-top: 0.25rem; } .nav-dropdown-menu a { diff --git a/app/templates/gallery.html b/app/templates/gallery.html index 8e65a02..572b62f 100644 --- a/app/templates/gallery.html +++ b/app/templates/gallery.html @@ -175,6 +175,7 @@ }; + diff --git a/app/templates/showcase.html b/app/templates/showcase.html index 2e94fd0..d153362 100644 --- a/app/templates/showcase.html +++ b/app/templates/showcase.html @@ -47,35 +47,12 @@ {% endfor %}] - - + +{% include '_gallery_modal.html' %} {% endblock %} {% block scripts %} + {% endblock %} diff --git a/dockerfile b/dockerfile deleted file mode 100644 index 6f85541..0000000 --- a/dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# Dockerfile -FROM python:slim - -WORKDIR /app - -# System deps for archives and video thumbs (all FOSS) -RUN apt-get update && apt-get install -y --no-install-recommends \ - p7zip-full \ - unar \ - file \ - ffmpeg \ - && rm -rf /var/lib/apt/lists/* - - -# Install deps first for better layer caching -COPY requirements.txt . -RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt - -# Copy the rest of the app -COPY . . - -# Entrypoint controls migrations, background worker, and Gunicorn -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -EXPOSE 5000 -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/summary.md b/summary.md index 57757da..59dc73c 100644 --- a/summary.md +++ b/summary.md @@ -4,7 +4,7 @@ A Flask-based image repository with Celery task processing for importing, organi > **IMPORTANT**: This summary must be kept up to date with any code changes. Update the timestamp below when making modifications. > -> **Last Updated**: 2026-02-04 (Pixiv sidecar support, Tags navbar dropdown, tag search) +> **Last Updated**: 2026-02-05 (Consolidated JS utilities, shared modal template, Tags dropdown gap fix) --- @@ -346,25 +346,40 @@ Import tasks support two modes based on `context.deep_scan`: | Template | Purpose | |----------|---------| -| `layout.html` | Base template with navbar | -| `showcase.html` | Random image masonry view | -| `gallery.html` | Main gallery with infinite scroll, timeline sidebar, bulk editor, series editor | +| `layout.html` | Base template with navbar (includes Tags dropdown) | +| `showcase.html` | Random image masonry view (extends layout, includes `_gallery_modal.html`) | +| `gallery.html` | Main gallery with infinite scroll, timeline sidebar, bulk editor (includes `_gallery_modal.html`) | | `reader.html` | Series reader with vertical scroll, page jump, thumbnail navigation | -| `tags_list.html` | Tag browser with preview images | +| `tags_list.html` | Tag browser with preview images and infinite scroll | | `settings.html` | Import settings, deletion tools, duplicate detection, maintenance tools | | `_gallery_item.html` | Single gallery item partial (with tag overlay icons) | -| `_gallery_modal.html` | Image modal with tag editor and series management | -| `_pagination.html` | Pagination controls | +| `_gallery_modal.html` | **Shared** image modal with tag editor and series management (used by gallery & showcase) | +| `_tag_cards.html` | Tag card grid partial for tag list | +| `_pagination.html` | Legacy pagination controls | +| `_pagination_floating.html` | Floating pagination indicator | + +**Template Inheritance**: +``` +layout.html (base) +├── gallery.html (includes: _gallery_item.html, _gallery_modal.html) +├── showcase.html (includes: _gallery_modal.html) +├── reader.html +├── tags_list.html (includes: _tag_cards.html) +└── settings.html +``` ### JavaScript Files (`app/static/js/`) | File | Purpose | |------|---------| -| `gallery-infinite.js` | Infinite scroll, timeline navigation | -| `view-modal.js` | Modal image navigation, tag editing, series management (add/update/move) | -| `bulk-select.js` | Multi-select with ordered selection, bulk tag editing, tag refresh, series bulk-add | +| `tag-utils.js` | **Shared utility module** - `getTagIcon()`, `getTagDisplayName()`, `escapeHtml()`, `createTagChipHtml()` used by all tag-related JS | +| `gallery-infinite.js` | Infinite scroll, timeline navigation (uses TagUtils) | +| `view-modal.js` | Modal image navigation, tag editing, series management (uses TagUtils) | +| `bulk-select.js` | Multi-select with ordered selection, bulk tag editing, tag refresh (uses TagUtils) | | `tag-editor.js` | Tag autocomplete and editing | -| `showcase.js` | Showcase shuffle functionality | +| `showcase.js` | Showcase masonry layout and shuffle functionality (uses TagUtils) | + +**Script Loading Order**: `tag-utils.js` must be loaded before other JS files that depend on `window.TagUtils`. ---