diff --git a/app/static/js/view-modal.js b/app/static/js/view-modal.js
index 5dda426..7563d28 100644
--- a/app/static/js/view-modal.js
+++ b/app/static/js/view-modal.js
@@ -28,8 +28,22 @@ document.addEventListener('DOMContentLoaded', () => {
const addSeriesPageNumber = document.getElementById('addSeriesPageNumber');
const addToSeriesBtn = document.getElementById('addToSeriesBtn');
const addToSeriesStatus = document.getElementById('addToSeriesStatus');
+ const addToSeriesToggle = document.getElementById('addToSeriesToggle');
+ const addToSeriesBody = document.getElementById('addToSeriesBody');
let seriesTagsLoaded = false;
+ // In-context feedback: show a brief message next to an element, auto-fades
+ function showFeedback(el, message, isError = false) {
+ if (!el) return;
+ el.textContent = message;
+ el.style.color = isError ? 'var(--flash-danger)' : 'var(--flash-success)';
+ el.classList.add('visible');
+ clearTimeout(el._feedbackTimeout);
+ el._feedbackTimeout = setTimeout(() => {
+ el.classList.remove('visible');
+ }, 1500);
+ }
+
// Autocomplete state
let autocompleteItems = [];
let autocompleteSelectedIndex = -1;
@@ -133,12 +147,15 @@ document.addEventListener('DOMContentLoaded', () => {
function hideSeriesInfo() {
if (seriesInfoEl) seriesInfoEl.style.display = 'none';
currentSeriesPageId = null;
- if (pageUpdateStatus) pageUpdateStatus.textContent = '';
+ if (pageUpdateStatus) { pageUpdateStatus.textContent = ''; pageUpdateStatus.classList.remove('visible'); }
}
function hideAddToSeries() {
if (addToSeriesEl) addToSeriesEl.style.display = 'none';
- if (addToSeriesStatus) addToSeriesStatus.textContent = '';
+ if (addToSeriesStatus) { addToSeriesStatus.textContent = ''; addToSeriesStatus.classList.remove('visible'); }
+ // Reset toggle state
+ if (addToSeriesToggle) addToSeriesToggle.classList.remove('open');
+ if (addToSeriesBody) addToSeriesBody.style.display = 'none';
}
async function loadSeriesTags() {
@@ -169,25 +186,10 @@ document.addEventListener('DOMContentLoaded', () => {
function showAddToSeries(seriesPage = null) {
if (!addToSeriesEl) return;
- loadSeriesTags().then(() => {
- // Pre-fill if image is already in a series
- if (seriesPage && seriesSelect) {
- seriesSelect.value = seriesPage.series_tag_id;
- if (addSeriesPageNumber) addSeriesPageNumber.value = seriesPage.page_number;
- if (addToSeriesBtn) {
- addToSeriesBtn.disabled = false;
- addToSeriesBtn.textContent = 'Update';
- }
- } else {
- if (seriesSelect) seriesSelect.value = '';
- if (addSeriesPageNumber) addSeriesPageNumber.value = '1';
- if (addToSeriesBtn) {
- addToSeriesBtn.disabled = true;
- addToSeriesBtn.textContent = 'Add';
- }
- }
- });
- if (addToSeriesStatus) addToSeriesStatus.textContent = '';
+ // Store pending pre-fill data for when the toggle is opened
+ addToSeriesEl._pendingSeriesPage = seriesPage || null;
+ if (addToSeriesStatus) { addToSeriesStatus.textContent = ''; addToSeriesStatus.classList.remove('visible'); }
+ // Show the outer container; inner body stays hidden until toggle click
addToSeriesEl.style.display = 'block';
}
@@ -200,7 +202,7 @@ document.addEventListener('DOMContentLoaded', () => {
const isUpdate = addToSeriesBtn?.textContent === 'Update';
if (addToSeriesBtn) addToSeriesBtn.disabled = true;
- if (addToSeriesStatus) addToSeriesStatus.textContent = isUpdate ? 'Updating...' : 'Adding...';
+ showFeedback(addToSeriesStatus, isUpdate ? 'Updating…' : 'Adding…');
try {
const r = await fetch(`/api/image/${imageId}/add-to-series`, {
@@ -211,18 +213,15 @@ document.addEventListener('DOMContentLoaded', () => {
const j = await r.json();
if (j.ok) {
- if (addToSeriesStatus) {
- addToSeriesStatus.textContent = '✓ Saved';
- setTimeout(() => { addToSeriesStatus.textContent = ''; }, 2000);
- }
+ showFeedback(addToSeriesStatus, 'Saved');
// Fully reload series info to show updated management interface
await loadSeriesInfo(imageId);
} else {
- if (addToSeriesStatus) addToSeriesStatus.textContent = j.error || 'Error';
+ showFeedback(addToSeriesStatus, j.error || 'Error', true);
if (addToSeriesBtn) addToSeriesBtn.disabled = false;
}
} catch (e) {
- if (addToSeriesStatus) addToSeriesStatus.textContent = 'Error';
+ showFeedback(addToSeriesStatus, 'Error', true);
if (addToSeriesBtn) addToSeriesBtn.disabled = false;
}
}
@@ -245,7 +244,7 @@ document.addEventListener('DOMContentLoaded', () => {
pageNumberInput.value = seriesPage.page_number;
}
- if (pageUpdateStatus) pageUpdateStatus.textContent = '';
+ if (pageUpdateStatus) { pageUpdateStatus.textContent = ''; pageUpdateStatus.classList.remove('visible'); }
seriesInfoEl.style.display = 'block';
}
@@ -279,12 +278,12 @@ document.addEventListener('DOMContentLoaded', () => {
if (!currentSeriesPageId || !pageNumberInput) return;
const newPageNumber = parseInt(pageNumberInput.value);
if (isNaN(newPageNumber) || newPageNumber < 1) {
- if (pageUpdateStatus) pageUpdateStatus.textContent = 'Invalid';
+ showFeedback(pageUpdateStatus, 'Invalid', true);
return;
}
if (updatePageBtn) updatePageBtn.disabled = true;
- if (pageUpdateStatus) pageUpdateStatus.textContent = 'Saving...';
+ showFeedback(pageUpdateStatus, 'Saving…');
try {
const fd = new FormData();
@@ -295,15 +294,12 @@ document.addEventListener('DOMContentLoaded', () => {
});
const j = await r.json();
if (j.ok) {
- if (pageUpdateStatus) {
- pageUpdateStatus.textContent = '✓';
- setTimeout(() => { pageUpdateStatus.textContent = ''; }, 2000);
- }
+ showFeedback(pageUpdateStatus, 'Saved');
} else {
- if (pageUpdateStatus) pageUpdateStatus.textContent = j.error || 'Error';
+ showFeedback(pageUpdateStatus, j.error || 'Error', true);
}
} catch (e) {
- if (pageUpdateStatus) pageUpdateStatus.textContent = 'Error';
+ showFeedback(pageUpdateStatus, 'Error', true);
} finally {
if (updatePageBtn) updatePageBtn.disabled = false;
}
@@ -354,6 +350,35 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
+ // Toggle "Add to Series" collapse
+ if (addToSeriesToggle) {
+ addToSeriesToggle.addEventListener('click', () => {
+ const isOpen = addToSeriesToggle.classList.toggle('open');
+ addToSeriesBody.style.display = isOpen ? 'block' : 'none';
+ // Load series options on first open, then apply any pending pre-fill
+ if (isOpen) {
+ const seriesPage = addToSeriesEl._pendingSeriesPage;
+ loadSeriesTags().then(() => {
+ if (seriesPage && seriesSelect) {
+ seriesSelect.value = seriesPage.series_tag_id;
+ if (addSeriesPageNumber) addSeriesPageNumber.value = seriesPage.page_number;
+ if (addToSeriesBtn) {
+ addToSeriesBtn.disabled = false;
+ addToSeriesBtn.textContent = 'Update';
+ }
+ } else {
+ if (seriesSelect) seriesSelect.value = '';
+ if (addSeriesPageNumber) addSeriesPageNumber.value = '1';
+ if (addToSeriesBtn) {
+ addToSeriesBtn.disabled = true;
+ addToSeriesBtn.textContent = 'Add';
+ }
+ }
+ });
+ }
+ });
+ }
+
async function addTag(imageId, name) {
const fd = new FormData();
fd.append('name', name);
@@ -361,6 +386,7 @@ document.addEventListener('DOMContentLoaded', () => {
const j = await r.json();
if (j.ok) {
await loadTags(imageId);
+ showFeedback(document.getElementById('tagActionFeedback'), 'Tag added');
return true;
}
return false;
@@ -372,6 +398,7 @@ document.addEventListener('DOMContentLoaded', () => {
const j = await r.json();
if (j.ok) {
await loadTags(imageId);
+ showFeedback(document.getElementById('tagActionFeedback'), 'Tag removed');
return true;
}
return false;
diff --git a/app/static/style.css b/app/static/style.css
index 2114c33..989a8ce 100644
--- a/app/static/style.css
+++ b/app/static/style.css
@@ -598,6 +598,73 @@ header {
.modal-close-hint { display: none; }
}
+/* Modal sidebar sections */
+.modal-sidebar-section {
+ padding: 0.75rem 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.07);
+}
+.modal-sidebar-section:last-child {
+ border-bottom: none;
+}
+
+/* Series toggle button (for "Add to Series" collapsed state) */
+.modal-series-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ font-size: 0.85rem;
+ cursor: pointer;
+ padding: 0.25rem 0;
+ transition: color 0.15s ease;
+}
+.modal-series-toggle:hover {
+ color: var(--text-dim);
+}
+.modal-series-toggle .toggle-arrow {
+ transition: transform 0.15s ease;
+ font-size: 0.9rem;
+}
+.modal-series-toggle.open .toggle-arrow {
+ transform: rotate(90deg);
+}
+
+.add-series-body {
+ margin-top: 0.5rem;
+}
+
+/* In-context feedback label */
+.tag-feedback {
+ font-size: 0.75rem;
+ color: var(--flash-success);
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ display: inline-block;
+ margin-left: 0.4rem;
+}
+.tag-feedback.visible {
+ opacity: 1;
+}
+.tag-feedback-inline {
+ display: block;
+ margin-left: 0;
+ margin-top: 0.3rem;
+ min-height: 1.1em;
+}
+
+/* Tag chip remove button — hidden at rest, visible on hover */
+.tag-chip .x {
+ opacity: 0;
+ transition: opacity 0.15s ease;
+ margin-left: 4px;
+}
+.tag-chip:hover .x {
+ opacity: 1;
+}
+
/*------------------------------------------------------------------------------
Tag overlays & editor
------------------------------------------------------------------------------*/
diff --git a/app/templates/_gallery_modal.html b/app/templates/_gallery_modal.html
index b724c00..69b373e 100644
--- a/app/templates/_gallery_modal.html
+++ b/app/templates/_gallery_modal.html
@@ -11,40 +11,55 @@
-