diff --git a/static/js/gallery.js b/static/js/gallery.js
index a8d927951..bc5d408c1 100644
--- a/static/js/gallery.js
+++ b/static/js/gallery.js
@@ -1278,6 +1278,7 @@ function _renderEditorLanding() {
Start a blank canvas, or open a photo from your gallery to edit it.
+
@@ -1328,6 +1329,28 @@ function _renderEditorLanding() {
document.getElementById('gallery-editor-pick')?.addEventListener('click', () => {
document.querySelector('#gallery-modal .gallery-tab[data-tab="images"]')?.click();
});
+ document.getElementById('gallery-editor-import')?.addEventListener('click', () => {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = 'image/*';
+ input.addEventListener('change', () => {
+ const file = input.files && input.files[0];
+ if (!file) return;
+ const name = (file.name || 'Imported image').replace(/\.[^.]+$/, '') || 'Imported image';
+ const reader = new FileReader();
+ reader.onload = async () => {
+ try {
+ await openEditor(reader.result, null, null, name);
+ } catch (e) {
+ console.error('[gallery] image import failed', e);
+ uiModule?.showError?.('Could not import that image');
+ }
+ };
+ reader.onerror = () => uiModule?.showError?.('Could not read that file');
+ reader.readAsDataURL(file);
+ });
+ input.click();
+ });
}
// Wire the first-tile Upload affordance in the Photos grid. Opens the same
@@ -1351,6 +1374,11 @@ function _wireUploadTile() {
});
input.click();
});
+ tile.addEventListener('keydown', (e) => {
+ if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
+ e.preventDefault();
+ tile.click();
+ });
}
// Shimmer placeholder tiles shown while the FIRST page loads, so the grid
@@ -1464,6 +1492,14 @@ function _renderGrid() {
const img = _items.find(i => i.id === card.dataset.id);
if (img) _openDetail(img);
});
+ card.addEventListener('keydown', (e) => {
+ if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
+ e.preventDefault();
+ const selectBtn = document.getElementById('gallery-select-btn');
+ if (selectBtn && selectBtn.classList.contains('active')) return;
+ const img = _items.find(i => i.id === card.dataset.id);
+ if (img) _openDetail(img);
+ });
});
// Download buttons
@@ -2251,6 +2287,7 @@ export function openGallery() {
Edit
×
+
+
+
Image model
+
Model used for AI edits and generated content in the editor (Quick Edit). Shared with the Quick Edit model picker.
+
+
@@ -2358,6 +2402,9 @@ export function openGallery() {
if (editorTabClose) {
editorTabClose.addEventListener('click', async (e) => {
e.stopPropagation();
+ // With several document tabs open the × closes only the active one and
+ // switches to its neighbour; the editor itself stays open.
+ if (window.geEditorCloseActiveDocument?.()) return;
if (isEditorOpen()) {
const ok = await uiModule.styledConfirm(
'Close the edit? Any unsaved changes will be lost.',
@@ -2558,6 +2605,52 @@ export function openGallery() {
});
}
+ // ── Image model (Settings tab) ──
+ // Mirror of the Quick Edit picker: same source (GET /api/editor/image-models)
+ // and the SAME localStorage key `ge-ai-command-model` (value `::`)
+ // so the two stay in sync.
+ const _GE_MODEL_KEY = 'ge-ai-command-model';
+ const imageModelSelect = document.getElementById('gallery-image-model');
+ if (imageModelSelect) {
+ const _fillImageModels = async () => {
+ try {
+ const r = await fetch(`${API_BASE}/api/editor/image-models`, { credentials: 'same-origin' });
+ const data = r.ok ? await r.json() : null;
+ const models = (data && Array.isArray(data.models)) ? data.models : [];
+ const saved = localStorage.getItem(_GE_MODEL_KEY) || '';
+ imageModelSelect.innerHTML = '';
+ const def = document.createElement('option');
+ def.value = '';
+ def.textContent = 'Default (configured provider)';
+ imageModelSelect.appendChild(def);
+ for (const m of models) {
+ const opt = document.createElement('option');
+ opt.value = `${m.base_url || ''}::${m.id}`;
+ opt.textContent = `${m.id} · ${m.provider || ''} (${m.kind || 'edit'})`.trim();
+ imageModelSelect.appendChild(opt);
+ }
+ imageModelSelect.value = saved;
+ if (imageModelSelect.value !== saved) imageModelSelect.value = '';
+ } catch (e) {
+ imageModelSelect.innerHTML = '';
+ }
+ };
+ imageModelSelect.addEventListener('change', () => {
+ const val = imageModelSelect.value || '';
+ try {
+ if (val) localStorage.setItem(_GE_MODEL_KEY, val);
+ else localStorage.removeItem(_GE_MODEL_KEY);
+ } catch (e) {}
+ window.dispatchEvent(new CustomEvent('ge-ai-command-model-change', { detail: { value: val } }));
+ });
+ _fillImageModels();
+ }
+ window.addEventListener('storage', (e) => {
+ if (e && e.key === _GE_MODEL_KEY && imageModelSelect) {
+ imageModelSelect.value = e.newValue || '';
+ }
+ });
+
// ── Tag All Untagged ──
let _tagCancelRequested = false;
let _tagging = false;