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