Skip to content
This repository was archived by the owner on Dec 30, 2025. It is now read-only.

Commit d37463b

Browse files
committed
Add batch matching queue system with visual enhancements
- NEW: Batch match page for queuing multiple book pairs - Added real-time progress tracking with cover art - Single unified progress bar - Auto-refresh dashboard - Improved visual design with gradients
1 parent 3d868dd commit d37463b

File tree

3 files changed

+508
-2
lines changed

3 files changed

+508
-2
lines changed

templates/batch_match.html

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Batch Match - Queue System</title>
5+
<style>
6+
* { box-sizing: border-box; }
7+
body {
8+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
9+
margin: 0;
10+
padding: 20px;
11+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12+
min-height: 100vh;
13+
}
14+
.container {
15+
max-width: 1600px;
16+
margin: 0 auto;
17+
background: white;
18+
padding: 30px;
19+
border-radius: 12px;
20+
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
21+
}
22+
h1 {
23+
color: #333;
24+
margin: 0 0 10px 0;
25+
}
26+
.subtitle {
27+
color: #666;
28+
margin: 0 0 20px 0;
29+
}
30+
.btn {
31+
padding: 12px 24px;
32+
background: #667eea;
33+
color: white;
34+
border: none;
35+
border-radius: 6px;
36+
cursor: pointer;
37+
font-size: 14px;
38+
font-weight: 600;
39+
transition: all 0.3s;
40+
text-decoration: none;
41+
display: inline-block;
42+
margin-right: 10px;
43+
}
44+
.btn:hover {
45+
background: #5568d3;
46+
transform: translateY(-2px);
47+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
48+
}
49+
.btn-secondary { background: #666; }
50+
.btn-secondary:hover { background: #555; }
51+
.btn-success { background: #4CAF50; }
52+
.btn-success:hover { background: #45a049; }
53+
.btn-danger { background: #f44336; }
54+
.btn-danger:hover { background: #da190b; }
55+
56+
.layout {
57+
display: grid;
58+
grid-template-columns: 1fr 400px;
59+
gap: 30px;
60+
margin-top: 30px;
61+
}
62+
63+
.selection-area {
64+
background: #f8f9fa;
65+
padding: 20px;
66+
border-radius: 8px;
67+
}
68+
69+
.queue-area {
70+
background: #f8f9fa;
71+
padding: 20px;
72+
border-radius: 8px;
73+
position: sticky;
74+
top: 20px;
75+
height: fit-content;
76+
max-height: calc(100vh - 40px);
77+
overflow-y: auto;
78+
}
79+
80+
.queue-header {
81+
display: flex;
82+
justify-content: space-between;
83+
align-items: center;
84+
margin-bottom: 15px;
85+
}
86+
87+
.queue-count {
88+
background: #667eea;
89+
color: white;
90+
padding: 4px 12px;
91+
border-radius: 12px;
92+
font-size: 12px;
93+
font-weight: 600;
94+
}
95+
96+
.queue-item {
97+
background: white;
98+
border: 1px solid #e0e0e0;
99+
border-radius: 8px;
100+
padding: 15px;
101+
margin-bottom: 10px;
102+
display: flex;
103+
gap: 10px;
104+
align-items: center;
105+
}
106+
107+
.queue-item-cover {
108+
width: 40px;
109+
height: 60px;
110+
object-fit: cover;
111+
border-radius: 4px;
112+
flex-shrink: 0;
113+
}
114+
115+
.queue-item-info {
116+
flex: 1;
117+
min-width: 0;
118+
}
119+
120+
.queue-item-title {
121+
font-size: 13px;
122+
font-weight: 600;
123+
color: #333;
124+
margin: 0 0 4px 0;
125+
white-space: nowrap;
126+
overflow: hidden;
127+
text-overflow: ellipsis;
128+
}
129+
130+
.queue-item-ebook {
131+
font-size: 11px;
132+
color: #666;
133+
margin: 0;
134+
white-space: nowrap;
135+
overflow: hidden;
136+
text-overflow: ellipsis;
137+
}
138+
139+
.queue-item-remove {
140+
background: #f44336;
141+
color: white;
142+
border: none;
143+
width: 24px;
144+
height: 24px;
145+
border-radius: 50%;
146+
cursor: pointer;
147+
font-size: 16px;
148+
line-height: 1;
149+
flex-shrink: 0;
150+
}
151+
152+
.queue-actions {
153+
margin-top: 15px;
154+
padding-top: 15px;
155+
border-top: 1px solid #e0e0e0;
156+
}
157+
158+
.search-box {
159+
width: 100%;
160+
padding: 15px;
161+
margin: 10px 0 20px 0;
162+
font-size: 16px;
163+
border: 2px solid #e0e0e0;
164+
border-radius: 8px;
165+
}
166+
167+
.matching-grid {
168+
display: grid;
169+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
170+
gap: 15px;
171+
margin: 20px 0;
172+
}
173+
174+
.book-card {
175+
border: 2px solid #e0e0e0;
176+
border-radius: 8px;
177+
padding: 10px;
178+
text-align: center;
179+
cursor: pointer;
180+
transition: all 0.3s;
181+
background: white;
182+
}
183+
184+
.book-card:hover {
185+
border-color: #667eea;
186+
transform: translateY(-2px);
187+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
188+
}
189+
190+
.book-card.selected {
191+
border-color: #667eea;
192+
background: #f0f4ff;
193+
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
194+
}
195+
196+
.book-cover {
197+
width: 100%;
198+
height: 200px;
199+
object-fit: cover;
200+
border-radius: 6px;
201+
margin-bottom: 8px;
202+
}
203+
204+
.book-cover-placeholder {
205+
width: 100%;
206+
height: 200px;
207+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
208+
border-radius: 6px;
209+
display: flex;
210+
align-items: center;
211+
justify-content: center;
212+
color: white;
213+
font-size: 48px;
214+
margin-bottom: 8px;
215+
}
216+
217+
.book-title {
218+
font-size: 12px;
219+
font-weight: 600;
220+
color: #333;
221+
word-wrap: break-word;
222+
line-height: 1.3;
223+
}
224+
225+
.section-title {
226+
font-size: 18px;
227+
font-weight: 600;
228+
color: #333;
229+
margin: 20px 0 10px 0;
230+
}
231+
232+
select.ebook-select {
233+
width: 100%;
234+
padding: 10px;
235+
border: 2px solid #e0e0e0;
236+
border-radius: 6px;
237+
font-size: 14px;
238+
margin: 10px 0;
239+
}
240+
241+
.add-to-queue-btn {
242+
width: 100%;
243+
margin-top: 10px;
244+
}
245+
246+
.empty-queue {
247+
text-align: center;
248+
padding: 40px 20px;
249+
color: #999;
250+
}
251+
252+
.empty-queue-icon {
253+
font-size: 48px;
254+
margin-bottom: 10px;
255+
opacity: 0.3;
256+
}
257+
</style>
258+
</head>
259+
<body>
260+
<div class="container">
261+
<h1>📚 Batch Match - Queue System</h1>
262+
<p class="subtitle">Select multiple audiobooks and ebooks, then process them all at once</p>
263+
264+
<div>
265+
<a href="/" class="btn btn-secondary">← Back to Dashboard</a>
266+
<a href="/match" class="btn btn-secondary">Single Match</a>
267+
</div>
268+
269+
<div class="layout">
270+
<!-- Selection Area -->
271+
<div class="selection-area">
272+
<form method="GET" action="/batch-match">
273+
<input type="text" name="search" placeholder="🔍 Filter by title..." class="search-box" value="{{ search }}">
274+
<button type="submit" class="btn">Search</button>
275+
</form>
276+
277+
<form method="POST" action="/batch-match" id="addToQueueForm">
278+
<input type="hidden" name="action" value="add_to_queue">
279+
<input type="hidden" name="search" value="{{ search }}">
280+
<input type="hidden" name="audiobook_id" id="selected_audiobook_id">
281+
<input type="hidden" name="ebook_filename" id="selected_ebook_filename">
282+
283+
<h3 class="section-title">1. Select Audiobook</h3>
284+
<div class="matching-grid">
285+
{% for ab in audiobooks %}
286+
<div class="book-card" onclick="selectAudiobook(this, '{{ ab.id }}', '{{ get_title(ab) }}')">
287+
{% if ab.cover_url %}
288+
<img src="{{ ab.cover_url }}" alt="Cover" class="book-cover" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
289+
<div class="book-cover-placeholder" style="display:none;">🎧</div>
290+
{% else %}
291+
<div class="book-cover-placeholder">🎧</div>
292+
{% endif %}
293+
<div class="book-title">{{ get_title(ab) }}</div>
294+
</div>
295+
{% endfor %}
296+
</div>
297+
298+
<h3 class="section-title">2. Select Ebook</h3>
299+
<select class="ebook-select" id="ebook_select" onchange="selectEbook(this.value)">
300+
<option value="">-- Choose Ebook --</option>
301+
{% for eb in ebooks %}
302+
<option value="{{ eb.name }}">{{ eb.name }}</option>
303+
{% endfor %}
304+
</select>
305+
306+
<button type="submit" class="btn btn-success add-to-queue-btn" id="addToQueueBtn" disabled>
307+
➕ Add to Queue
308+
</button>
309+
</form>
310+
</div>
311+
312+
<!-- Queue Area -->
313+
<div class="queue-area">
314+
<div class="queue-header">
315+
<h3 style="margin: 0;">Queue</h3>
316+
<span class="queue-count">{{ queue|length }} items</span>
317+
</div>
318+
319+
{% if queue %}
320+
{% for item in queue %}
321+
<div class="queue-item">
322+
<img src="{{ item.cover_url }}" alt="Cover" class="queue-item-cover" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 40 60%22%3E%3Crect fill=%22%23667eea%22 width=%2240%22 height=%2260%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 dominant-baseline=%22middle%22 text-anchor=%22middle%22 font-size=%2224%22 fill=%22white%22%3E📖%3C/text%3E%3C/svg%3E';">
323+
<div class="queue-item-info">
324+
<p class="queue-item-title" title="{{ item.abs_title }}">{{ item.abs_title }}</p>
325+
<p class="queue-item-ebook" title="{{ item.ebook_filename }}">{{ item.ebook_filename }}</p>
326+
</div>
327+
<form method="POST" action="/batch-match" style="display:inline;">
328+
<input type="hidden" name="action" value="remove_from_queue">
329+
<input type="hidden" name="abs_id" value="{{ item.abs_id }}">
330+
<button type="submit" class="queue-item-remove">×</button>
331+
</form>
332+
</div>
333+
{% endfor %}
334+
335+
<div class="queue-actions">
336+
<form method="POST" action="/batch-match" style="margin-bottom: 10px;">
337+
<input type="hidden" name="action" value="process_queue">
338+
<button type="submit" class="btn btn-success" style="width: 100%;">
339+
✅ Process All ({{ queue|length }})
340+
</button>
341+
</form>
342+
343+
<form method="POST" action="/batch-match">
344+
<input type="hidden" name="action" value="clear_queue">
345+
<button type="submit" class="btn btn-danger" style="width: 100%;" onclick="return confirm('Clear entire queue?')">
346+
🗑️ Clear Queue
347+
</button>
348+
</form>
349+
</div>
350+
{% else %}
351+
<div class="empty-queue">
352+
<div class="empty-queue-icon">📋</div>
353+
<p>Queue is empty<br>Add items to get started</p>
354+
</div>
355+
{% endif %}
356+
</div>
357+
</div>
358+
</div>
359+
360+
<script>
361+
let selectedAudiobookId = null;
362+
let selectedEbookFilename = null;
363+
364+
function selectAudiobook(element, id, title) {
365+
// Remove selected class from all audiobooks
366+
document.querySelectorAll('.book-card').forEach(el => {
367+
el.classList.remove('selected');
368+
});
369+
370+
// Add selected class to clicked audiobook
371+
element.classList.add('selected');
372+
selectedAudiobookId = id;
373+
374+
// Update hidden input
375+
document.getElementById('selected_audiobook_id').value = id;
376+
377+
// Enable button if both selected
378+
updateAddButton();
379+
}
380+
381+
function selectEbook(filename) {
382+
selectedEbookFilename = filename;
383+
document.getElementById('selected_ebook_filename').value = filename;
384+
updateAddButton();
385+
}
386+
387+
function updateAddButton() {
388+
const btn = document.getElementById('addToQueueBtn');
389+
if (selectedAudiobookId && selectedEbookFilename) {
390+
btn.disabled = false;
391+
} else {
392+
btn.disabled = true;
393+
}
394+
}
395+
</script>
396+
</body>
397+
</html>

0 commit comments

Comments
 (0)