feat(sets): added filter on sets page to show duplicate sets. default is shown. can be hidden using env var. works with consolidated sets too.

This commit is contained in:
2025-10-03 09:13:15 +02:00
parent 4b653ac270
commit a3d08d8cf6
12 changed files with 196 additions and 7 deletions
+4
View File
@@ -358,6 +358,10 @@
# Default: false
# BK_SHOW_GRID_SORT=true
# Optional: Show duplicate filter button on sets page
# Default: true
# BK_SHOW_SETS_DUPLICATE_FILTER=true
# Optional: Skip saving or displaying spare parts
# Default: false
# BK_SKIP_SPARE_PARTS=true
+1
View File
@@ -82,6 +82,7 @@ CONFIG: Final[list[dict[str, Any]]] = [
{'n': 'SETS_CONSOLIDATION', 'd': False, 'c': bool},
{'n': 'SHOW_GRID_FILTERS', 'c': bool},
{'n': 'SHOW_GRID_SORT', 'c': bool},
{'n': 'SHOW_SETS_DUPLICATE_FILTER', 'd': True, 'c': bool},
{'n': 'SKIP_SPARE_PARTS', 'c': bool},
{'n': 'SOCKET_NAMESPACE', 'd': 'bricksocket'},
{'n': 'SOCKET_PATH', 'd': '/bricksocket/'},
+1
View File
@@ -44,6 +44,7 @@ LIVE_CHANGEABLE_VARS: Final[List[str]] = [
'BK_REBRICKABLE_LINKS',
'BK_SHOW_GRID_FILTERS',
'BK_SHOW_GRID_SORT',
'BK_SHOW_SETS_DUPLICATE_FILTER',
'BK_SKIP_SPARE_PARTS',
'BK_USE_REMOTE_IMAGES',
'BK_PEERON_DOWNLOAD_DELAY',
+3 -1
View File
@@ -86,6 +86,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
storage_filter: str | None = None,
tag_filter: str | None = None,
year_filter: str | None = None,
duplicate_filter: bool = False,
use_consolidated: bool = True
) -> tuple[Self, int]:
# Convert theme name to theme ID for filtering
@@ -94,7 +95,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
theme_id_filter = self._theme_name_to_id(theme_filter)
# Check if any filters are applied
has_filters = any([status_filter, theme_id_filter, owner_filter, purchase_location_filter, storage_filter, tag_filter, year_filter])
has_filters = any([status_filter, theme_id_filter, owner_filter, purchase_location_filter, storage_filter, tag_filter, year_filter, duplicate_filter])
# Prepare filter context
filter_context = {
@@ -106,6 +107,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
'storage_filter': storage_filter,
'tag_filter': tag_filter,
'year_filter': year_filter,
'duplicate_filter': duplicate_filter,
'owners': BrickSetOwnerList.as_columns(),
'statuses': BrickSetStatusList.as_columns(),
'tags': BrickSetTagList.as_columns(),
@@ -70,4 +70,12 @@ AND EXISTS (
)
{% endif %}
{% endif %}
{% if duplicate_filter %}
AND (
SELECT COUNT(*)
FROM "bricktracker_sets" as "duplicate_check"
WHERE "duplicate_check"."set" = "bricktracker_sets"."set"
) > 1
{% endif %}
{% endblock %}
+10 -4
View File
@@ -147,15 +147,21 @@ AND NOT EXISTS (
GROUP BY "rebrickable_sets"."set"
{% if status_filter or duplicate_filter %}
HAVING 1=1
{% if status_filter %}
{% if status_filter == 'has-missing' %}
HAVING IFNULL(SUM("problem_join"."total_missing"), 0) > 0
AND IFNULL(SUM("problem_join"."total_missing"), 0) > 0
{% elif status_filter == '-has-missing' %}
HAVING IFNULL(SUM("problem_join"."total_missing"), 0) = 0
AND IFNULL(SUM("problem_join"."total_missing"), 0) = 0
{% elif status_filter == 'has-damaged' %}
HAVING IFNULL(SUM("problem_join"."total_damaged"), 0) > 0
AND IFNULL(SUM("problem_join"."total_damaged"), 0) > 0
{% elif status_filter == '-has-damaged' %}
HAVING IFNULL(SUM("problem_join"."total_damaged"), 0) = 0
AND IFNULL(SUM("problem_join"."total_damaged"), 0) = 0
{% endif %}
{% endif %}
{% if duplicate_filter %}
AND COUNT("bricktracker_sets"."id") > 1
{% endif %}
{% endif %}
+3
View File
@@ -47,6 +47,7 @@ def list() -> str:
storage_filter = request.args.get('storage')
tag_filter = request.args.get('tag')
year_filter = request.args.get('year')
duplicate_filter = request.args.get('duplicate', '').lower() == 'true'
# Get pagination configuration
per_page, is_mobile = get_pagination_config('sets')
@@ -67,6 +68,7 @@ def list() -> str:
storage_filter=storage_filter,
tag_filter=tag_filter,
year_filter=year_filter,
duplicate_filter=duplicate_filter,
use_consolidated=current_app.config['SETS_CONSOLIDATION']
)
@@ -102,6 +104,7 @@ def list() -> str:
'current_storage_filter': storage_filter,
'current_tag_filter': tag_filter,
'current_year_filter': year_filter,
'current_duplicate_filter': duplicate_filter,
'brickset_statuses': BrickSetStatusList.list(),
**set_metadata_lists(as_class=True)
}
+6 -1
View File
@@ -180,7 +180,12 @@ class BrickGridFilter {
}
// If we passed all filters, we need to display it
current.parentElement.classList.remove("d-none");
// But also check if it's hidden by duplicate filter
if (!current.parentElement.classList.contains("duplicate-filter-hidden")) {
current.parentElement.classList.remove("d-none");
} else {
current.parentElement.classList.add("d-none");
}
});
}
}
+145
View File
@@ -19,6 +19,9 @@ document.addEventListener("DOMContentLoaded", () => {
const searchInput = document.getElementById('grid-search');
const searchClear = document.getElementById('grid-search-clear');
// Initialize duplicate filter functionality
initializeDuplicateFilter();
if (searchInput && searchClear) {
if (isPaginationMode()) {
// PAGINATION MODE - Server-side search
@@ -559,4 +562,146 @@ function removeSetGrouping() {
groupContainers.forEach(container => {
container.remove();
});
}
// Initialize duplicate/consolidated filter functionality
function initializeDuplicateFilter() {
const duplicateFilterButton = document.getElementById('duplicate-filter-toggle');
if (!duplicateFilterButton) return;
// Check if the filter should be active from URL parameters
const urlParams = new URLSearchParams(window.location.search);
const isDuplicateFilterActive = urlParams.get('duplicate') === 'true';
// Set initial button state
if (isDuplicateFilterActive) {
duplicateFilterButton.classList.remove('btn-outline-secondary');
duplicateFilterButton.classList.add('btn-secondary');
}
duplicateFilterButton.addEventListener('click', () => {
const isCurrentlyActive = duplicateFilterButton.classList.contains('btn-secondary');
const newState = !isCurrentlyActive;
// Update button appearance
if (newState) {
duplicateFilterButton.classList.remove('btn-outline-secondary');
duplicateFilterButton.classList.add('btn-secondary');
} else {
duplicateFilterButton.classList.remove('btn-secondary');
duplicateFilterButton.classList.add('btn-outline-secondary');
}
if (isPaginationMode()) {
// SERVER-SIDE MODE - Update URL parameter
performDuplicateFilterServer(newState);
} else {
// CLIENT-SIDE MODE - Apply filtering directly
applyDuplicateFilter(newState);
}
});
}
// Server-side duplicate filter
function performDuplicateFilterServer(showOnlyDuplicates) {
const currentUrl = new URL(window.location);
if (showOnlyDuplicates) {
currentUrl.searchParams.set('duplicate', 'true');
} else {
currentUrl.searchParams.delete('duplicate');
}
// Reset to page 1 when filtering
currentUrl.searchParams.set('page', '1');
window.location.href = currentUrl.toString();
}
// Apply duplicate/consolidated filter
function applyDuplicateFilter(showOnlyDuplicates) {
// Get the grid container and all column containers (not just the cards)
const gridContainer = document.getElementById('grid');
if (!gridContainer) {
console.warn('Grid container not found');
return;
}
// Try multiple selectors to find column containers
let columnContainers = gridContainer.querySelectorAll('.col-md-6');
if (columnContainers.length === 0) {
columnContainers = gridContainer.querySelectorAll('[class*="col-"]');
}
if (!showOnlyDuplicates) {
// Show all column containers by removing the duplicate-filter-hidden class
columnContainers.forEach(col => {
col.classList.remove('duplicate-filter-hidden');
});
// Trigger the existing grid filter to refresh
triggerGridRefresh();
return;
}
// Check if we're in consolidated mode by looking for data-instance-count
const consolidatedMode = document.querySelector('[data-instance-count]') !== null;
if (consolidatedMode) {
// CONSOLIDATED MODE: Show only sets with instance count > 1
columnContainers.forEach(col => {
const card = col.querySelector('[data-set-id]');
if (card) {
const instanceCount = parseInt(card.dataset.instanceCount || '1');
if (instanceCount > 1) {
col.classList.remove('duplicate-filter-hidden');
} else {
col.classList.add('duplicate-filter-hidden');
}
}
});
} else {
// NON-CONSOLIDATED MODE: Show only sets that appear multiple times
const setByCounts = {};
// Count occurrences of each set
columnContainers.forEach(col => {
const card = col.querySelector('[data-set-id]');
if (card) {
const rebrickableId = card.dataset.rebrickableId;
if (rebrickableId) {
setByCounts[rebrickableId] = (setByCounts[rebrickableId] || 0) + 1;
}
}
});
// Show/hide based on count
columnContainers.forEach(col => {
const card = col.querySelector('[data-set-id]');
if (card) {
const rebrickableId = card.dataset.rebrickableId;
if (rebrickableId && setByCounts[rebrickableId] > 1) {
col.classList.remove('duplicate-filter-hidden');
} else {
col.classList.add('duplicate-filter-hidden');
}
}
});
}
// Trigger the existing grid filter to refresh and respect our duplicate filter
triggerGridRefresh();
}
// Helper function to trigger grid filter refresh
function triggerGridRefresh() {
// Check if we have a grid instance with filter capability
if (window.gridInstances) {
const gridElement = document.getElementById('grid');
if (gridElement && window.gridInstances[gridElement.id]) {
const gridInstance = window.gridInstances[gridElement.id];
if (gridInstance.filter) {
// Trigger the existing filter to refresh
gridInstance.filter.filter();
}
}
}
}
+2 -1
View File
@@ -178,4 +178,5 @@
border-radius: 2px;
opacity: 0.8;
pointer-events: none;
}
}/* Duplicate filter support */
.duplicate-filter-hidden { display: none !important; }
+8
View File
@@ -236,6 +236,14 @@
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_SHOW_SETS_DUPLICATE_FILTER" data-var="BK_SHOW_SETS_DUPLICATE_FILTER">
<label class="form-check-label" for="BK_SHOW_SETS_DUPLICATE_FILTER">
BK_SHOW_SETS_DUPLICATE_FILTER {{ config_badges('BK_SHOW_SETS_DUPLICATE_FILTER') }}
<div class="text-muted small">Show duplicate/consolidated filter button on sets page</div>
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input config-toggle" type="checkbox" id="BK_INDEPENDENT_ACCORDIONS" data-var="BK_INDEPENDENT_ACCORDIONS">
<label class="form-check-label" for="BK_INDEPENDENT_ACCORDIONS">
+5
View File
@@ -22,6 +22,11 @@
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#grid-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="grid-filter">
<i class="ri-filter-line"></i> Filters
</button>
{% if config['SHOW_SETS_DUPLICATE_FILTER'] %}
<button class="btn {% if current_duplicate_filter %}btn-secondary{% else %}btn-outline-secondary{% endif %}" type="button" id="duplicate-filter-toggle" title="Show duplicate sets only">
<i class="ri-stack-line"></i> Duplicates
</button>
{% endif %}
</div>
</div>
</div>