feat: Introduce Shaarli Professional Theme with a modern sidebar layout and light/dark mode.
This commit is contained in:
parent
646f92005f
commit
0b6b45b1d7
@ -3113,3 +3113,62 @@ select:focus {
|
||||
.header-action-btn.has-active-filter {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
/* Filter Info Banner */
|
||||
.filter-info-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
color: white;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
margin: 1rem 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.filter-info-banner.empty-results {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
}
|
||||
|
||||
.filter-info-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.filter-info-content i {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.filter-info-content strong {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-clear-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.filter-clear-btn i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@ -4,8 +4,7 @@
|
||||
<meta name="referrer" content="same-origin">
|
||||
<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" />
|
||||
<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
|
||||
title="Shaarli search - {$shaarlititle}" />
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" title="Shaarli search - {$shaarlititle}" />
|
||||
|
||||
<!-- Professional Theme CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="{$asset_path}/css/style.css#" />
|
||||
@ -26,13 +25,15 @@
|
||||
{/if}
|
||||
|
||||
<script>
|
||||
var shaarli = {
|
||||
basePath: '{$base_path}',
|
||||
rootPath: '{$root_path}',
|
||||
assetPath: '{$asset_path}',
|
||||
isAuth: {if="$is_logged_in"}true{else}false{/if},
|
||||
pageName: '{$pageName}'
|
||||
};
|
||||
var shaarli = {
|
||||
basePath: '{$base_path}',
|
||||
rootPath: '{$root_path}',
|
||||
assetPath: '{$asset_path}',
|
||||
isAuth: {if="$is_logged_in"}true{else}false{/if},
|
||||
pageName: '{$pageName}',
|
||||
visibility: '{$visibility}',
|
||||
untaggedonly: {if="$untaggedonly"}true{else}false{/if}
|
||||
};
|
||||
</script>
|
||||
<script src="{$asset_path}/js/script.js#" defer></script>
|
||||
|
||||
|
||||
@ -402,38 +402,69 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
// Handle filter toggle switches
|
||||
// Logic:
|
||||
// - Visibility: all (neither checked), private (only private checked), public (only public checked)
|
||||
// - Untagged: can be combined with any visibility
|
||||
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 + '/';
|
||||
|
||||
// Get current filter state from server-side rendered variables
|
||||
const currentVisibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : '';
|
||||
const currentUntagged = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) || false;
|
||||
|
||||
console.log('[Filter] Applying filters - private:', isPrivate, 'public:', isPublic, 'untagged:', isUntagged);
|
||||
console.log('[Filter] Current state - visibility:', currentVisibility, 'untagged:', currentUntagged);
|
||||
|
||||
// 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';
|
||||
let url = basePath + '/';
|
||||
|
||||
// Determine desired visibility
|
||||
let desiredVisibility = 'all';
|
||||
if (isPrivate) {
|
||||
desiredVisibility = 'private';
|
||||
} else if (isPublic) {
|
||||
url = basePath + '/admin/visibility/public';
|
||||
} else if (isUntagged) {
|
||||
url = basePath + '/?searchtags=';
|
||||
desiredVisibility = 'public';
|
||||
}
|
||||
|
||||
// Build URL based on desired state
|
||||
if (desiredVisibility === 'private') {
|
||||
url = basePath + '/admin/visibility/private';
|
||||
} else if (desiredVisibility === 'public') {
|
||||
url = basePath + '/admin/visibility/public';
|
||||
} else {
|
||||
// visibility = all - need to clear visibility if it was set
|
||||
if (currentVisibility && currentVisibility !== '') {
|
||||
url = basePath + '/admin/visibility/all';
|
||||
} else if (isUntagged !== currentUntagged) {
|
||||
// Untagged state changed - /untagged-only works as toggle
|
||||
url = basePath + '/untagged-only';
|
||||
} else {
|
||||
url = basePath + '/';
|
||||
}
|
||||
}
|
||||
// else: no filter, url stays as basePath + '/'
|
||||
|
||||
console.log('[Filter] Navigating to:', url);
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// Clear all filters and go to home
|
||||
// Clear all filters - go back to showing all bookmarks
|
||||
function clearAllFilters() {
|
||||
const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : '';
|
||||
window.location.href = basePath + '/';
|
||||
const currentVisibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : '';
|
||||
const currentUntagged = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) || false;
|
||||
|
||||
// If visibility is set, clear it first
|
||||
if (currentVisibility && currentVisibility !== '') {
|
||||
window.location.href = basePath + '/admin/visibility/all';
|
||||
} else if (currentUntagged) {
|
||||
// Toggle untagged off using /untagged-only
|
||||
window.location.href = basePath + '/untagged-only';
|
||||
} else {
|
||||
window.location.href = basePath + '/';
|
||||
}
|
||||
}
|
||||
|
||||
// Private and public are mutually exclusive
|
||||
@ -455,24 +486,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
// Initialize filter states from URL (detect current filter state from page URL)
|
||||
// Initialize filter states from server-side variables (set in includes.html)
|
||||
(function initFilterStates() {
|
||||
console.log('[Filter Debug] ========================================');
|
||||
|
||||
// Detect filter state from current URL path
|
||||
const currentPath = window.location.pathname;
|
||||
const currentSearch = window.location.search;
|
||||
// Get filter state from server-side rendered variables
|
||||
const visibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : '';
|
||||
const untaggedonly = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) ? shaarli.untaggedonly : false;
|
||||
|
||||
// Detect visibility filter from URL path
|
||||
let isPrivateActive = currentPath.includes('/visibility/private') || currentPath.includes('/admin/visibility/private');
|
||||
let isPublicActive = currentPath.includes('/visibility/public') || currentPath.includes('/admin/visibility/public');
|
||||
// Detect active filters from server variables
|
||||
let isPrivateActive = visibility === 'private';
|
||||
let isPublicActive = visibility === 'public';
|
||||
let isUntaggedActive = untaggedonly === true;
|
||||
|
||||
// Detect untagged filter
|
||||
let isUntaggedActive = currentPath.includes('/untagged-only') ||
|
||||
(currentSearch.includes('searchtags=') && !currentSearch.match(/searchtags=[^&]+/));
|
||||
|
||||
console.log('[Filter Debug] URL path:', currentPath);
|
||||
console.log('[Filter Debug] URL search:', currentSearch);
|
||||
console.log('[Filter Debug] shaarli.visibility:', visibility);
|
||||
console.log('[Filter Debug] shaarli.untaggedonly:', untaggedonly);
|
||||
|
||||
console.log('[Filter Debug] isPrivateActive:', isPrivateActive);
|
||||
console.log('[Filter Debug] isPublicActive:', isPublicActive);
|
||||
@ -512,45 +540,66 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Create and display the filter info banner
|
||||
if (hasActiveFilter && !document.getElementById('filter-info-banner')) {
|
||||
// Get result count from page
|
||||
let resultCount = '';
|
||||
// Get result count from page - try multiple sources
|
||||
let resultCount = 0;
|
||||
const pagingStats = document.querySelector('.paging-stats strong:last-child');
|
||||
const linkCount = document.querySelectorAll('.link-outer').length;
|
||||
|
||||
if (pagingStats) {
|
||||
resultCount = pagingStats.textContent;
|
||||
resultCount = parseInt(pagingStats.textContent) || 0;
|
||||
} else if (linkCount > 0) {
|
||||
resultCount = linkCount;
|
||||
}
|
||||
|
||||
// Build the message with result count like "X results with status private"
|
||||
let filterParts = [];
|
||||
// Check if no results (empty state)
|
||||
const emptyState = document.querySelector('.empty-state');
|
||||
const isEmptyResults = emptyState !== null || resultCount === 0;
|
||||
|
||||
// Build the message like the examples:
|
||||
// "5 results without any tag"
|
||||
// "5 results with status private without any tag"
|
||||
// "1 result with status public without any tag"
|
||||
let statusPart = '';
|
||||
let untaggedPart = '';
|
||||
|
||||
if (isPrivateActive) {
|
||||
filterParts.push('with status <strong>private</strong>');
|
||||
statusPart = '<strong>private</strong>';
|
||||
} else if (isPublicActive) {
|
||||
filterParts.push('with status <strong>public</strong>');
|
||||
statusPart = '<strong>public</strong>';
|
||||
}
|
||||
|
||||
if (isUntaggedActive) {
|
||||
if (filterParts.length > 0) {
|
||||
filterParts.push('and <strong>untagged</strong>');
|
||||
} else {
|
||||
filterParts.push('<strong>untagged</strong>');
|
||||
untaggedPart = '<strong>without any tag</strong>';
|
||||
}
|
||||
|
||||
// Build the message
|
||||
let message = '';
|
||||
if (isEmptyResults) {
|
||||
message = 'Nothing found.';
|
||||
} else {
|
||||
const resultWord = resultCount === 1 ? 'result' : 'results';
|
||||
if (statusPart && untaggedPart) {
|
||||
message = `${resultCount} ${resultWord} with status ${statusPart} ${untaggedPart}`;
|
||||
} else if (statusPart) {
|
||||
message = `${resultCount} ${resultWord} with status ${statusPart}`;
|
||||
} else if (untaggedPart) {
|
||||
message = `${resultCount} ${resultWord} ${untaggedPart}`;
|
||||
}
|
||||
}
|
||||
|
||||
let message = resultCount ? resultCount + ' results ' + filterParts.join(' ') : 'Showing ' + filterParts.join(' ') + ' links';
|
||||
|
||||
console.log('[Filter Debug] filterParts:', filterParts);
|
||||
console.log('[Filter Debug] resultCount:', resultCount);
|
||||
console.log('[Filter Debug] isEmptyResults:', isEmptyResults);
|
||||
console.log('[Filter Debug] message:', message);
|
||||
|
||||
if (filterParts.length > 0) {
|
||||
if (message) {
|
||||
// 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.className = 'filter-info-banner' + (isEmptyResults ? ' empty-results' : '');
|
||||
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">
|
||||
@ -562,19 +611,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Try to find the best insertion point
|
||||
const contentToolbar = document.querySelector('.content-toolbar');
|
||||
const linklist = document.getElementById('linklist');
|
||||
const linksList = document.getElementById('links-list');
|
||||
const emptyStateDiv = document.querySelector('.empty-state');
|
||||
|
||||
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 (emptyStateDiv && emptyStateDiv.parentNode) {
|
||||
emptyStateDiv.parentNode.insertBefore(banner, emptyStateDiv);
|
||||
console.log('[Filter Debug] Banner inserted before empty-state');
|
||||
} else if (linklist) {
|
||||
linklist.insertBefore(banner, linklist.firstChild);
|
||||
console.log('[Filter Debug] Banner inserted at beginning of linklist');
|
||||
@ -584,8 +632,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Add click handler to clear button
|
||||
document.getElementById('filter-clear-btn')?.addEventListener('click', () => {
|
||||
console.log('[Filter] Clear button clicked, navigating to home');
|
||||
window.location.href = basePath + '/';
|
||||
console.log('[Filter] Clear button clicked, clearing all filters');
|
||||
// Clear based on what was active
|
||||
if (isPrivateActive || isPublicActive) {
|
||||
window.location.href = basePath + '/admin/visibility/all';
|
||||
} else if (isUntaggedActive) {
|
||||
// Toggle untagged off
|
||||
window.location.href = basePath + '/untagged-only';
|
||||
} else {
|
||||
window.location.href = basePath + '/';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user