Major refactor: deduplicate routes, sync, JS, and fix bugs
- Extract 8 common route patterns into factory functions in routes/shared.py (favourite, upload, replace cover, save defaults, clone, save JSON, get missing, clear covers) — removes ~1,100 lines across 9 route files - Extract generic _sync_category() in sync.py — 7 sync functions become one-liner wrappers, removing ~350 lines - Extract shared detail page JS into static/js/detail-common.js — all 9 detail templates now call initDetailPage() with minimal config - Extract layout inline JS into static/js/layout-utils.js (~185 lines) - Extract library toolbar JS into static/js/library-toolbar.js - Fix finalize missing-image bug: raise RuntimeError instead of logging warning so job is marked failed - Fix missing scheduler default in _default_checkpoint_data() - Fix N+1 query in Character.get_available_outfits() with batch IN query - Convert all print() to logger across services and routes - Add missing tags display to styles, scenes, detailers, checkpoints detail - Update delete buttons to use trash.png icon with solid red background - Update CLAUDE.md to reflect new architecture Net reduction: ~1,600 lines Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -113,20 +113,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Selected Preview -->
|
||||
<div class="card mb-3" id="preview-pane" {% if not preview_path %}style="display:none"{% endif %}>
|
||||
<div class="card-header d-flex justify-content-between align-items-center py-1">
|
||||
<small class="fw-semibold">Selected Preview</small>
|
||||
</div>
|
||||
<div class="card-body p-1">
|
||||
<img id="preview-img" src="{{ url_for('static', filename='uploads/' + preview_path) if preview_path else '' }}"
|
||||
class="img-fluid rounded" alt="Preview">
|
||||
</div>
|
||||
<div class="card-footer p-2">
|
||||
<form action="{{ url_for('replace_preset_cover_from_preview', slug=preset.slug) }}" method="post">
|
||||
<div class="card mb-4 {% if preview_path %}border-success{% else %}border-secondary d-none{% endif %}" id="preview-card">
|
||||
<div class="card-header {% if preview_path %}bg-success{% else %}bg-secondary{% endif %} text-white d-flex justify-content-between align-items-center" id="preview-card-header">
|
||||
<span>Selected Preview</span>
|
||||
<form action="{{ url_for('replace_preset_cover_from_preview', slug=preset.slug) }}" method="post" class="m-0" id="replace-cover-form">
|
||||
<input type="hidden" name="preview_path" id="preview-path" value="{{ preview_path or '' }}">
|
||||
<button type="submit" class="btn btn-sm btn-warning w-100">Set as Cover</button>
|
||||
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" {% if not preview_path %}disabled{% endif %}>Replace Cover</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;">
|
||||
<img id="preview-img" src="{{ url_for('static', filename='uploads/' + preview_path) if preview_path else '' }}" alt="Preview" class="img-fluid">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -304,119 +303,12 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/detail-common.js"></script>
|
||||
<script>
|
||||
// Job polling
|
||||
let currentJobId = null;
|
||||
|
||||
document.getElementById('generate-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const btn = e.submitter;
|
||||
const actionVal = btn.value;
|
||||
const formData = new FormData(this);
|
||||
formData.set('action', actionVal);
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Generating...';
|
||||
|
||||
fetch(this.getAttribute('action'), {
|
||||
method: 'POST',
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
body: formData
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.job_id) {
|
||||
currentJobId = data.job_id;
|
||||
pollJob(currentJobId, btn, actionVal);
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = btn.dataset.label || 'Generate Preview';
|
||||
alert('Error: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(() => { btn.disabled = false; });
|
||||
});
|
||||
|
||||
function pollJob(jobId, btn, actionVal) {
|
||||
fetch('/api/queue/' + jobId + '/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'done' && data.result) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = btn.getAttribute('data-orig') || btn.textContent.replace('Generating...', 'Generate Preview');
|
||||
// Add to gallery
|
||||
const img = document.createElement('img');
|
||||
img.src = data.result.image_url;
|
||||
img.className = 'img-fluid rounded';
|
||||
img.style.cursor = 'pointer';
|
||||
img.dataset.previewPath = data.result.relative_path;
|
||||
img.addEventListener('click', () => selectPreview(data.result.relative_path, img.src));
|
||||
const col = document.createElement('div');
|
||||
col.className = 'col-4 col-md-3';
|
||||
col.appendChild(img);
|
||||
document.getElementById('generated-images').prepend(col);
|
||||
document.getElementById('no-images-msg')?.classList.add('d-none');
|
||||
selectPreview(data.result.relative_path, data.result.image_url);
|
||||
updateSeedFromResult(data.result);
|
||||
} else if (data.status === 'failed') {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Generate Preview';
|
||||
alert('Generation failed: ' + (data.error || 'Unknown error'));
|
||||
} else {
|
||||
setTimeout(() => pollJob(jobId, btn, actionVal), 1500);
|
||||
}
|
||||
})
|
||||
.catch(() => setTimeout(() => pollJob(jobId, btn, actionVal), 3000));
|
||||
}
|
||||
|
||||
function selectPreview(relativePath, imageUrl) {
|
||||
document.getElementById('preview-path').value = relativePath;
|
||||
document.getElementById('preview-img').src = imageUrl;
|
||||
document.getElementById('preview-pane').style.display = '';
|
||||
}
|
||||
|
||||
function showImage(src) {
|
||||
if (src) document.getElementById('modalImage').src = src;
|
||||
}
|
||||
|
||||
// Delegate click on generated images
|
||||
document.addEventListener('click', function(e) {
|
||||
const img = e.target.closest('img[data-preview-path]');
|
||||
if (img) selectPreview(img.dataset.previewPath, img.src);
|
||||
});
|
||||
|
||||
window._onEndlessResult = function(jobResult) {
|
||||
if (jobResult.result?.image_url) {
|
||||
selectPreview(jobResult.result.relative_path, jobResult.result.image_url);
|
||||
const img = document.createElement('img');
|
||||
img.src = jobResult.result.image_url;
|
||||
img.className = 'img-fluid rounded';
|
||||
img.style.cursor = 'pointer';
|
||||
img.dataset.previewPath = jobResult.result.relative_path;
|
||||
img.addEventListener('click', () => selectPreview(jobResult.result.relative_path, img.src));
|
||||
const col = document.createElement('div');
|
||||
col.className = 'col-4 col-md-3';
|
||||
col.appendChild(img);
|
||||
document.getElementById('generated-images').prepend(col);
|
||||
document.getElementById('no-images-msg')?.classList.add('d-none');
|
||||
}
|
||||
};
|
||||
|
||||
// Resolution preset buttons
|
||||
document.querySelectorAll('.res-preset').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.getElementById('res-width').value = btn.dataset.w;
|
||||
document.getElementById('res-height').value = btn.dataset.h;
|
||||
document.querySelectorAll('.res-preset').forEach(b => {
|
||||
b.classList.remove('btn-secondary');
|
||||
b.classList.add('btn-outline-secondary');
|
||||
});
|
||||
btn.classList.remove('btn-outline-secondary');
|
||||
btn.classList.add('btn-secondary');
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initDetailPage({
|
||||
jsonEditorUrl: "{{ url_for('save_preset_json', slug=preset.slug) }}"
|
||||
});
|
||||
});
|
||||
|
||||
// JSON editor
|
||||
initJsonEditor("{{ url_for('save_preset_json', slug=preset.slug) }}");
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user