5858 box-shadow : 0 4px 12px rgba (244 , 67 , 54 , 0.4 );
5959 }
6060
61+ .controls-container {
62+ display : flex;
63+ gap : 20px ;
64+ align-items : center;
65+ margin : 20px 0 ;
66+ flex-wrap : wrap;
67+ }
68+
69+ .control-group {
70+ display : flex;
71+ align-items : center;
72+ gap : 10px ;
73+ }
74+
75+ .control-group label {
76+ font-size : 14px ;
77+ font-weight : 600 ;
78+ color : # 555 ;
79+ }
80+
81+ select , input [type = "checkbox" ] {
82+ padding : 6px 10px ;
83+ border-radius : 6px ;
84+ border : 2px solid # e0e0e0 ;
85+ font-size : 14px ;
86+ transition : border-color 0.3s ;
87+ }
88+
89+ select : focus {
90+ outline : none;
91+ border-color : # 667eea ;
92+ }
93+
94+ input [type = "checkbox" ] {
95+ width : 18px ;
96+ height : 18px ;
97+ cursor : pointer;
98+ }
99+
100+ .filter-toggle {
101+ display : flex;
102+ align-items : center;
103+ gap : 8px ;
104+ padding : 8px 16px ;
105+ background : # f8f9fa ;
106+ border-radius : 6px ;
107+ border : 2px solid # e0e0e0 ;
108+ cursor : pointer;
109+ transition : all 0.3s ;
110+ }
111+
112+ .filter-toggle : hover {
113+ border-color : # 667eea ;
114+ background : # f0f4ff ;
115+ }
116+
117+ .filter-toggle .active {
118+ background : # 667eea ;
119+ color : white;
120+ border-color : # 667eea ;
121+ }
122+
123+ .filter-toggle input [type = "checkbox" ] {
124+ margin : 0 ;
125+ }
126+
127+ .filter-toggle label {
128+ margin : 0 ;
129+ cursor : pointer;
130+ font-size : 14px ;
131+ font-weight : 600 ;
132+ }
133+
61134 .mapping-grid {
62135 display : grid;
63136 grid-template-columns : repeat (auto-fill, minmax (350px , 1fr ));
@@ -230,9 +303,29 @@ <h1>📚 ABS-KoSync Manager</h1>
230303 < a href ="/batch-match " class ="btn btn-success "> 📋 Batch Match</ a >
231304
232305 {% if mappings %}
306+ <!-- Controls -->
307+ < div class ="controls-container ">
308+ < div class ="control-group ">
309+ < label for ="sort-select "> Sort by:</ label >
310+ < select id ="sort-select ">
311+ < option value ="title "> Title</ option >
312+ < option value ="progress "> Progress</ option >
313+ < option value ="status "> Status</ option >
314+ < option value ="last_sync "> Last Synced</ option >
315+ </ select >
316+ </ div >
317+
318+ < div class ="filter-toggle " id ="filter-toggle ">
319+ < input type ="checkbox " id ="filter-current ">
320+ < label for ="filter-current "> 📖 Show Only Currently Reading</ label >
321+ </ div >
322+ </ div >
323+
233324 < div class ="mapping-grid ">
234325 {% for mapping in mappings %}
235- < div class ="mapping-card ">
326+ < div class ="mapping-card "
327+ data-progress ="{{ mapping.kosync_progress }} "
328+ data-last-sync ="{{ mapping.last_sync }} ">
236329 < div class ="card-header ">
237330 {% if mapping.cover_url %}
238331 < img src ="{{ mapping.cover_url }} " alt ="Cover " class ="cover-image " onerror ="this.style.display='none'; this.nextElementSibling.style.display='flex'; ">
@@ -290,12 +383,113 @@ <h2>No mappings configured yet</h2>
290383 </ div >
291384 {% endif %}
292385 </ div >
293-
386+
294387 < script >
388+ const grid = document . querySelector ( '.mapping-grid' ) ;
389+ const sortSelect = document . getElementById ( 'sort-select' ) ;
390+ const filterCheckbox = document . getElementById ( 'filter-current' ) ;
391+ const filterToggle = document . getElementById ( 'filter-toggle' ) ;
392+
393+ // Load saved preferences
394+ const savedSort = localStorage . getItem ( 'abs_kosync_sort' ) || 'title' ;
395+ const savedFilter = localStorage . getItem ( 'abs_kosync_filter_current' ) === 'true' ;
396+
397+ if ( savedSort ) sortSelect . value = savedSort ;
398+ if ( savedFilter ) {
399+ filterCheckbox . checked = true ;
400+ filterToggle . classList . add ( 'active' ) ;
401+ }
402+
403+ // Convert "Xs ago", "Xm ago", "Xh ago" to seconds for sorting
404+ function parseLastSync ( syncText ) {
405+ if ( syncText === 'Never' ) return Infinity ;
406+ if ( syncText === 'Error' ) return Infinity ;
407+
408+ const match = syncText . match ( / ( \d + ) ( [ s m h ] ) / ) ;
409+ if ( ! match ) return Infinity ;
410+
411+ const value = parseInt ( match [ 1 ] ) ;
412+ const unit = match [ 2 ] ;
413+
414+ if ( unit === 's' ) return value ;
415+ if ( unit === 'm' ) return value * 60 ;
416+ if ( unit === 'h' ) return value * 3600 ;
417+
418+ return Infinity ;
419+ }
420+
421+ function sortCards ( sortBy ) {
422+ if ( ! grid ) return ;
423+ const cards = Array . from ( grid . children ) ;
424+
425+ const sortedCards = cards . sort ( ( a , b ) => {
426+ if ( sortBy === 'title' ) {
427+ return a . querySelector ( '.card-title' ) . innerText . localeCompare ( b . querySelector ( '.card-title' ) . innerText ) ;
428+ } else if ( sortBy === 'progress' ) {
429+ const aProg = parseFloat ( a . dataset . progress ) || 0 ;
430+ const bProg = parseFloat ( b . dataset . progress ) || 0 ;
431+ return bProg - aProg ; // descending
432+ } else if ( sortBy === 'status' ) {
433+ return a . querySelector ( '.status-badge' ) . innerText . localeCompare ( b . querySelector ( '.status-badge' ) . innerText ) ;
434+ } else if ( sortBy === 'last_sync' ) {
435+ const aSync = parseLastSync ( a . dataset . lastSync ) ;
436+ const bSync = parseLastSync ( b . dataset . lastSync ) ;
437+ return aSync - bSync ; // ascending (most recent first)
438+ }
439+ return 0 ;
440+ } ) ;
441+
442+ sortedCards . forEach ( card => grid . appendChild ( card ) ) ;
443+ }
444+
445+ function filterCards ( showOnlyCurrent ) {
446+ if ( ! grid ) return ;
447+ const cards = Array . from ( grid . children ) ;
448+
449+ cards . forEach ( card => {
450+ if ( showOnlyCurrent ) {
451+ const progress = parseFloat ( card . dataset . progress ) || 0 ;
452+ // Show if progress > 0 and < 100
453+ if ( progress > 0 && progress < 100 ) {
454+ card . style . display = '' ;
455+ } else {
456+ card . style . display = 'none' ;
457+ }
458+ } else {
459+ card . style . display = '' ;
460+ }
461+ } ) ;
462+ }
463+
464+ // Apply initial sort and filter
465+ sortCards ( sortSelect . value ) ;
466+ filterCards ( filterCheckbox . checked ) ;
467+
468+ // Sort dropdown change
469+ sortSelect . addEventListener ( 'change' , ( e ) => {
470+ const sortBy = e . target . value ;
471+ localStorage . setItem ( 'abs_kosync_sort' , sortBy ) ;
472+ sortCards ( sortBy ) ;
473+ } ) ;
474+
475+ // Filter checkbox change
476+ filterCheckbox . addEventListener ( 'change' , ( e ) => {
477+ const isChecked = e . target . checked ;
478+ localStorage . setItem ( 'abs_kosync_filter_current' , isChecked ) ;
479+ filterToggle . classList . toggle ( 'active' , isChecked ) ;
480+ filterCards ( isChecked ) ;
481+ } ) ;
482+
483+ // Make the whole toggle clickable
484+ filterToggle . addEventListener ( 'click' , ( e ) => {
485+ if ( e . target !== filterCheckbox ) {
486+ filterCheckbox . checked = ! filterCheckbox . checked ;
487+ filterCheckbox . dispatchEvent ( new Event ( 'change' ) ) ;
488+ }
489+ } ) ;
490+
295491 // Auto-refresh every 30 seconds
296- setTimeout ( function ( ) {
297- location . reload ( ) ;
298- } , 30000 ) ;
492+ setTimeout ( ( ) => location . reload ( ) , 30000 ) ;
299493 </ script >
300494</ body >
301- </ html >
495+ </ html >
0 commit comments