feat: add Shaarli Pro theme with new header, styling, and JavaScript.
This commit is contained in:
parent
16a76db547
commit
7fdf3edcf3
@ -1076,6 +1076,12 @@ input:checked+.theme-slider:before {
|
|||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.paging-total {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.paging a {
|
.paging a {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -2813,7 +2819,6 @@ select:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Print Styles ===== */
|
|
||||||
@media print {
|
@media print {
|
||||||
|
|
||||||
.sidebar,
|
.sidebar,
|
||||||
@ -2832,4 +2837,148 @@ select:focus {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: 1px solid #ddd;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -401,16 +401,212 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle filter toggle switches - using data-url like the example
|
// Handle filter toggle switches
|
||||||
document.querySelectorAll('.toggle-switch input[type="checkbox"]').forEach(checkbox => {
|
function applyFilters() {
|
||||||
checkbox.addEventListener('change', (e) => {
|
const isPrivate = filterPrivate?.checked || false;
|
||||||
const label = e.target.closest('label');
|
const isPublic = filterPublic?.checked || false;
|
||||||
if (label && label.dataset.url) {
|
const isUntagged = filterUntagged?.checked || false;
|
||||||
window.location.href = label.dataset.url;
|
|
||||||
}
|
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
|
// Handle links per page options
|
||||||
|
|
||||||
|
|
||||||
@ -421,7 +617,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const value = filterInput.value;
|
const value = filterInput.value;
|
||||||
if (value && value > 0) {
|
if (value && value > 0) {
|
||||||
window.location.href = shaarli.basePath + '/?nb=' + value;
|
window.location.href = shaarli.basePath + '/links-per-page?nb=' + value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,9 +131,9 @@
|
|||||||
<div class="filter-body">
|
<div class="filter-body">
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
<div class="filter-section-title">Links Per Page</div>
|
<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}/links-per-page?nb=20" class="filter-option">20 links</a>
|
||||||
<a href="{$base_path}/?nb=50" class="filter-option">50 links</a>
|
<a href="{$base_path}/links-per-page?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=100" class="filter-option">100 links</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
<div class="filter-section-title">Custom value</div>
|
<div class="filter-section-title">Custom value</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user