feat: add Shaarli Pro theme with new header, styling, and JavaScript.

This commit is contained in:
Bruno Charest 2026-01-14 12:29:13 -05:00
parent 16a76db547
commit 7fdf3edcf3
3 changed files with 358 additions and 13 deletions

View File

@ -1076,6 +1076,12 @@ input:checked+.theme-slider:before {
color: var(--text-main);
}
.paging-total {
color: var(--text-muted);
font-size: 0.85rem;
margin-left: 0.25rem;
}
.paging a {
display: flex;
align-items: center;
@ -2813,7 +2819,6 @@ select:focus {
}
}
/* ===== Print Styles ===== */
@media print {
.sidebar,
@ -2832,4 +2837,148 @@ select:focus {
box-shadow: none;
border: 1px solid #ddd;
}
}
/* ===== Filter Active Indicator ===== */
.header-action-btn.has-active-filter {
position: relative;
background: rgba(59, 130, 246, 0.3);
border: 1px solid rgba(59, 130, 246, 0.5);
}
.header-action-btn.has-active-filter:hover {
background: rgba(59, 130, 246, 0.4);
}
.filter-badge {
position: absolute;
top: 4px;
right: 4px;
width: 8px;
height: 8px;
background: #ef4444;
border-radius: 50%;
border: 2px solid var(--header-bg);
animation: pulse-badge 2s infinite;
}
@keyframes pulse-badge {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.8;
}
}
/* ===== Filter Info Banner ===== */
.filter-info-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.875rem 1.25rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(99, 102, 241, 0.08));
border: 1px solid rgba(59, 130, 246, 0.25);
border-radius: 0.75rem;
color: var(--text-main);
animation: slideInBanner 0.3s ease;
}
[data-theme="dark"] .filter-info-banner {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(99, 102, 241, 0.12));
border-color: rgba(59, 130, 246, 0.3);
}
@keyframes slideInBanner {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.filter-info-content {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.9rem;
}
.filter-info-content i {
font-size: 1.25rem;
color: #3b82f6;
}
.filter-info-content strong {
color: #3b82f6;
font-weight: 600;
}
[data-theme="dark"] .filter-info-content i,
[data-theme="dark"] .filter-info-content strong {
color: #60a5fa;
}
.filter-clear-btn {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.25);
border-radius: 0.5rem;
color: #ef4444;
font-size: 0.8125rem;
font-weight: 500;
font-family: inherit;
text-decoration: none;
transition: all 0.2s ease;
white-space: nowrap;
cursor: pointer;
}
.filter-clear-btn:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.4);
color: #dc2626;
}
[data-theme="dark"] .filter-clear-btn {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.3);
color: #f87171;
}
[data-theme="dark"] .filter-clear-btn:hover {
background: rgba(239, 68, 68, 0.25);
color: #fca5a5;
}
.filter-clear-btn i {
font-size: 1rem;
}
/* Responsive adjustments for filter banner */
@media (max-width: 600px) {
.filter-info-banner {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
}
.filter-clear-btn {
align-self: flex-end;
}
}

View File

@ -401,16 +401,212 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Handle filter toggle switches - using data-url like the example
document.querySelectorAll('.toggle-switch input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const label = e.target.closest('label');
if (label && label.dataset.url) {
window.location.href = label.dataset.url;
}
});
// Handle filter toggle switches
function applyFilters() {
const isPrivate = filterPrivate?.checked || false;
const isPublic = filterPublic?.checked || false;
const isUntagged = filterUntagged?.checked || false;
let basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : '';
let url = basePath + '/';
// Save filter state to localStorage before navigation
const filterState = {
private: isPrivate,
public: isPublic,
untagged: isUntagged,
timestamp: Date.now()
};
localStorage.setItem('shaarliFilterState', JSON.stringify(filterState));
console.log('[Filter] Saved filter state:', filterState);
// Build the URL based on selected filters using the correct Shaarli admin paths
if (isPrivate && isUntagged) {
url = basePath + '/admin/visibility/private?searchtags=';
} else if (isPublic && isUntagged) {
url = basePath + '/admin/visibility/public?searchtags=';
} else if (isPrivate) {
url = basePath + '/admin/visibility/private';
} else if (isPublic) {
url = basePath + '/admin/visibility/public';
} else if (isUntagged) {
url = basePath + '/untagged-only';
} else {
// No filter selected - clear the state
localStorage.removeItem('shaarliFilterState');
}
console.log('[Filter] Navigating to:', url);
window.location.href = url;
}
// Clear filters function
function clearFilters() {
localStorage.removeItem('shaarliFilterState');
if (filterPrivate) filterPrivate.checked = false;
if (filterPublic) filterPublic.checked = false;
if (filterUntagged) filterUntagged.checked = false;
}
// Private and public are mutually exclusive
filterPrivate?.addEventListener('change', (e) => {
if (e.target.checked && filterPublic) {
filterPublic.checked = false;
}
applyFilters();
});
filterPublic?.addEventListener('change', (e) => {
if (e.target.checked && filterPrivate) {
filterPrivate.checked = false;
}
applyFilters();
});
filterUntagged?.addEventListener('change', () => {
applyFilters();
});
// Initialize filter states from localStorage (since Shaarli redirects after applying filters)
(function initFilterStates() {
console.log('[Filter Debug] ========================================');
// Try to read filter state from localStorage
let savedState = null;
try {
const savedJson = localStorage.getItem('shaarliFilterState');
if (savedJson) {
savedState = JSON.parse(savedJson);
console.log('[Filter Debug] Found saved filter state:', savedState);
// Check if state is not too old (30 seconds max)
const age = Date.now() - (savedState.timestamp || 0);
if (age > 30000) {
console.log('[Filter Debug] Saved state is too old, ignoring');
localStorage.removeItem('shaarliFilterState');
savedState = null;
}
}
} catch (e) {
console.log('[Filter Debug] Error reading saved state:', e);
}
// Determine active filters from saved state
let isPrivateActive = savedState?.private || false;
let isPublicActive = savedState?.public || false;
let isUntaggedActive = savedState?.untagged || false;
console.log('[Filter Debug] isPrivateActive:', isPrivateActive);
console.log('[Filter Debug] isPublicActive:', isPublicActive);
console.log('[Filter Debug] isUntaggedActive:', isUntaggedActive);
// Set checkbox states
if (filterPrivate && isPrivateActive) {
filterPrivate.checked = true;
console.log('[Filter Debug] ✓ Set filterPrivate to CHECKED');
}
if (filterPublic && isPublicActive) {
filterPublic.checked = true;
console.log('[Filter Debug] ✓ Set filterPublic to CHECKED');
}
if (filterUntagged && isUntaggedActive) {
filterUntagged.checked = true;
console.log('[Filter Debug] ✓ Set filterUntagged to CHECKED');
}
const hasActiveFilter = isPrivateActive || isPublicActive || isUntaggedActive;
console.log('[Filter Debug] hasActiveFilter:', hasActiveFilter);
console.log('[Filter Debug] ========================================');
// Add/update filter indicator badge on the filter button
if (filterToggleBtn && hasActiveFilter) {
filterToggleBtn.classList.add('has-active-filter');
console.log('[Filter Debug] Added has-active-filter class to button');
// Add badge indicator if not exists
if (!filterToggleBtn.querySelector('.filter-badge')) {
const badge = document.createElement('span');
badge.className = 'filter-badge';
filterToggleBtn.appendChild(badge);
console.log('[Filter Debug] Added filter badge');
}
}
// Create and display the filter info banner
if (hasActiveFilter && !document.getElementById('filter-info-banner')) {
// Build the message - describe the active filter without count
let filterParts = [];
if (isPrivateActive) {
filterParts.push('<strong>private</strong> links');
} else if (isPublicActive) {
filterParts.push('<strong>public</strong> links');
}
if (isUntaggedActive) {
if (filterParts.length > 0) {
filterParts.push('without tags');
} else {
filterParts.push('links <strong>without tags</strong>');
}
}
let message = 'Showing ' + filterParts.join(' ');
console.log('[Filter Debug] filterParts:', filterParts);
console.log('[Filter Debug] message:', message);
if (filterParts.length > 0) {
// Get base path safely
const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : '';
const banner = document.createElement('div');
banner.id = 'filter-info-banner';
banner.className = 'filter-info-banner';
banner.innerHTML = `
<div class="filter-info-content">
<i class="mdi mdi-filter-variant"></i>
<span>${message}</span>
</div>
<button type="button" class="filter-clear-btn" id="filter-clear-btn" title="Clear filters">
<i class="mdi mdi-close"></i>
<span>Clear</span>
</button>
`;
// Try to find the best insertion point
const contentToolbar = document.querySelector('.content-toolbar');
const linklist = document.getElementById('linklist');
const linksList = document.getElementById('links-list');
console.log('[Filter Debug] contentToolbar:', contentToolbar);
console.log('[Filter Debug] linklist:', linklist);
console.log('[Filter Debug] linksList:', linksList);
// Insert after the content-toolbar (pagination)
if (contentToolbar && contentToolbar.parentNode) {
contentToolbar.parentNode.insertBefore(banner, contentToolbar.nextSibling);
console.log('[Filter Debug] Banner inserted after content-toolbar');
} else if (linksList && linksList.parentNode) {
linksList.parentNode.insertBefore(banner, linksList);
console.log('[Filter Debug] Banner inserted before links-list');
} else if (linklist) {
linklist.insertBefore(banner, linklist.firstChild);
console.log('[Filter Debug] Banner inserted at beginning of linklist');
} else {
console.log('[Filter Debug] Could not find insertion point for banner');
}
// Add click handler to clear button
document.getElementById('filter-clear-btn')?.addEventListener('click', () => {
localStorage.removeItem('shaarliFilterState');
console.log('[Filter] Cleared filter state from localStorage');
window.location.href = basePath + '/';
});
}
}
})();
// Handle links per page options
@ -421,7 +617,7 @@ document.addEventListener('DOMContentLoaded', () => {
e.preventDefault();
const value = filterInput.value;
if (value && value > 0) {
window.location.href = shaarli.basePath + '/?nb=' + value;
window.location.href = shaarli.basePath + '/links-per-page?nb=' + value;
}
});
}

View File

@ -131,9 +131,9 @@
<div class="filter-body">
<div class="filter-section">
<div class="filter-section-title">Links Per Page</div>
<a href="{$base_path}/?nb=20" class="filter-option">20 links</a>
<a href="{$base_path}/?nb=50" class="filter-option">50 links</a>
<a href="{$base_path}/?nb=100" class="filter-option">100 links</a>
<a href="{$base_path}/links-per-page?nb=20" class="filter-option">20 links</a>
<a href="{$base_path}/links-per-page?nb=50" class="filter-option">50 links</a>
<a href="{$base_path}/links-per-page?nb=100" class="filter-option">100 links</a>
</div>
<div class="filter-section">
<div class="filter-section-title">Custom value</div>