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

Commit c4f6960

Browse files
authored
Implement sorting and filtering controls in UI
Added controls for sorting and filtering mappings in the UI.
1 parent 395d1e0 commit c4f6960

File tree

1 file changed

+200
-6
lines changed

1 file changed

+200
-6
lines changed

templates/index.html

Lines changed: 200 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,79 @@
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+)([smh])/);
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

Comments
 (0)