diff --git a/app/main.py b/app/main.py index 7b2bf55..7dd9755 100644 --- a/app/main.py +++ b/app/main.py @@ -440,7 +440,8 @@ def gallery_jump(): ) -def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, search=None, null_fandom_only=False): +def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, search=None, null_fandom_only=False, + default_excluded_kinds=('archive',)): """ Helper function to get tags with counts and preview images. Returns (tag_data list, total_count, has_more). @@ -453,6 +454,11 @@ def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, sea count_q = db.session.query(func.count(Tag.id)) if kind: count_q = count_q.filter(Tag.kind == kind) + else: + # Hide noisy auto-generated kinds (archive, etc.) from the default view; + # users can still reach them via an explicit ?kind=archive filter. + if default_excluded_kinds: + count_q = count_q.filter(~Tag.kind.in_(default_excluded_kinds)) if search: count_q = count_q.filter(Tag.name.ilike(f'%{search}%')) if null_fandom_only: @@ -467,6 +473,9 @@ def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, sea if kind: q = q.filter(Tag.kind == kind) + else: + if default_excluded_kinds: + q = q.filter(~Tag.kind.in_(default_excluded_kinds)) if search: q = q.filter(Tag.name.ilike(f'%{search}%')) if null_fandom_only: diff --git a/app/static/js/tag-editor.js b/app/static/js/tag-editor.js index fa55de1..7a1a993 100644 --- a/app/static/js/tag-editor.js +++ b/app/static/js/tag-editor.js @@ -118,7 +118,12 @@ document.addEventListener('DOMContentLoaded', () => { if (data.ok) { tagIdInput.value = data.tag.id; - nameInput.value = data.tag.display_name; + // Use the bare name, not display_name — display_name for a character + // includes the "(Fandom)" suffix, which would make the name input's + // value a concatenation of the bare name and the fandom string. + // Saving that as-is would skip the rename-collision merge path and + // persist a malformed tag name. + nameInput.value = data.tag.name; kindSelect.value = data.tag.kind || 'user'; // Set fandom fields diff --git a/app/static/js/view-modal.js b/app/static/js/view-modal.js index a73f131..9084645 100644 --- a/app/static/js/view-modal.js +++ b/app/static/js/view-modal.js @@ -879,12 +879,30 @@ document.addEventListener('DOMContentLoaded', () => { }); } + let fandomPickerSelectedIdx = -1; + + function highlightFandomItem(idx) { + if (!fandomPickerAutocomplete) return; + const items = fandomPickerAutocomplete.querySelectorAll('.tag-autocomplete-item'); + items.forEach((el, i) => el.classList.toggle('selected', i === idx)); + fandomPickerSelectedIdx = idx; + } + + function pickFandomRow(row) { + if (!row || !fandomPickerInput) return; + fandomPickerInput.value = row.dataset.fandomName; + fandomPickerInput.dataset.fandomId = row.dataset.fandomId; + clearFandomAutocomplete(); + fandomPickerSelectedIdx = -1; + } + if (fandomPickerInput) { fandomPickerInput.addEventListener('input', () => { clearTimeout(fandomPickerDebounce); const term = fandomPickerInput.value.trim(); - // Any typing invalidates a previously-selected fandom id. + // Any typing invalidates a previously-selected fandom id + highlight. delete fandomPickerInput.dataset.fandomId; + fandomPickerSelectedIdx = -1; if (!term) { clearFandomAutocomplete(); return; @@ -896,6 +914,33 @@ document.addEventListener('DOMContentLoaded', () => { .catch(() => clearFandomAutocomplete()); }, 150); }); + + // Keyboard parity with the main tagInput: ArrowDown/Up to navigate rows, + // Enter to pick a highlighted row (or submit the whole form when no row + // is highlighted), Escape to clear the highlight. + fandomPickerInput.addEventListener('keydown', (e) => { + const rows = fandomPickerAutocomplete + ? fandomPickerAutocomplete.querySelectorAll('.tag-autocomplete-item') + : []; + const count = rows.length; + if (e.key === 'ArrowDown' && count > 0) { + e.preventDefault(); + highlightFandomItem(Math.min(fandomPickerSelectedIdx + 1, count - 1)); + } else if (e.key === 'ArrowUp' && count > 0) { + e.preventDefault(); + highlightFandomItem(Math.max(fandomPickerSelectedIdx - 1, 0)); + } else if (e.key === 'Enter') { + if (fandomPickerSelectedIdx >= 0 && rows[fandomPickerSelectedIdx]) { + e.preventDefault(); + pickFandomRow(rows[fandomPickerSelectedIdx]); + } else { + e.preventDefault(); + submitTag(); + } + } else if (e.key === 'Escape') { + highlightFandomItem(-1); + } + }); } if (fandomPickerAutocomplete) { @@ -903,11 +948,7 @@ document.addEventListener('DOMContentLoaded', () => { const row = e.target.closest('[data-fandom-id]'); if (!row) return; e.preventDefault(); // keep focus on the input - if (fandomPickerInput) { - fandomPickerInput.value = row.dataset.fandomName; - fandomPickerInput.dataset.fandomId = row.dataset.fandomId; - } - clearFandomAutocomplete(); + pickFandomRow(row); }); } diff --git a/app/static/style.css b/app/static/style.css index e66eccf..b50f98e 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -3046,24 +3046,39 @@ body.select-mode .gallery-infinite-container { opacity: 1; margin-top: 0.5rem; } -.fandom-picker-label { - display: block; - font-size: 0.85rem; - color: var(--muted, #888); - margin-bottom: 0.25rem; -} .fandom-picker-input-row { display: flex; - gap: 0.25rem; + gap: 0.5rem; align-items: center; } +/* Match the main .tag-form input styling so the picker feels like the + same control as the add-tag field above it. */ .fandom-picker-input { flex: 1; - padding: 0.4rem 0.6rem; - background: var(--input-bg, #111); - border: 1px solid var(--input-border, #333); - border-radius: 0.25rem; - color: var(--fg, #eee); + padding: 0.5rem 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 6px; + background: rgba(255, 255, 255, 0.08); + color: var(--text); + font-size: 16px; +} +.fandom-picker-input::placeholder { + color: var(--text-muted); +} +.fandom-picker-input:focus { + outline: none; + border-color: var(--btn-primary); + background: rgba(255, 255, 255, 0.12); +} + +/* Desktop: the form's submit button is redundant — Enter on the tag input + (and Enter in the fandom picker when no row is highlighted) already + submits. Keep the button visible on mobile where virtual keyboards make + Enter-to-submit less reliable. */ +@media (min-width: 768px) { + .tag-form > button[type="submit"] { + display: none; + } } /* Ambiguous-candidate picker (Task 24) */ diff --git a/app/templates/_gallery_modal.html b/app/templates/_gallery_modal.html index fb568b8..4815a75 100644 --- a/app/templates/_gallery_modal.html +++ b/app/templates/_gallery_modal.html @@ -62,10 +62,9 @@