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 {
|
.header-action-btn.has-active-filter {
|
||||||
background: rgba(239, 68, 68, 0.2);
|
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">
|
<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/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="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#"
|
<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" title="Shaarli search - {$shaarlititle}" />
|
||||||
title="Shaarli search - {$shaarlititle}" />
|
|
||||||
|
|
||||||
<!-- Professional Theme CSS -->
|
<!-- Professional Theme CSS -->
|
||||||
<link type="text/css" rel="stylesheet" href="{$asset_path}/css/style.css#" />
|
<link type="text/css" rel="stylesheet" href="{$asset_path}/css/style.css#" />
|
||||||
@ -26,13 +25,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var shaarli = {
|
var shaarli = {
|
||||||
basePath: '{$base_path}',
|
basePath: '{$base_path}',
|
||||||
rootPath: '{$root_path}',
|
rootPath: '{$root_path}',
|
||||||
assetPath: '{$asset_path}',
|
assetPath: '{$asset_path}',
|
||||||
isAuth: {if="$is_logged_in"}true{else}false{/if},
|
isAuth: {if="$is_logged_in"}true{else}false{/if},
|
||||||
pageName: '{$pageName}'
|
pageName: '{$pageName}',
|
||||||
};
|
visibility: '{$visibility}',
|
||||||
|
untaggedonly: {if="$untaggedonly"}true{else}false{/if}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="{$asset_path}/js/script.js#" defer></script>
|
<script src="{$asset_path}/js/script.js#" defer></script>
|
||||||
|
|
||||||
|
|||||||
@ -402,38 +402,69 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle filter toggle switches
|
// 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() {
|
function applyFilters() {
|
||||||
const isPrivate = filterPrivate?.checked || false;
|
const isPrivate = filterPrivate?.checked || false;
|
||||||
const isPublic = filterPublic?.checked || false;
|
const isPublic = filterPublic?.checked || false;
|
||||||
const isUntagged = filterUntagged?.checked || false;
|
const isUntagged = filterUntagged?.checked || false;
|
||||||
|
|
||||||
let basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : '';
|
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] 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
|
let url = basePath + '/';
|
||||||
if (isPrivate && isUntagged) {
|
|
||||||
url = basePath + '/admin/visibility/private?searchtags=';
|
// Determine desired visibility
|
||||||
} else if (isPublic && isUntagged) {
|
let desiredVisibility = 'all';
|
||||||
url = basePath + '/admin/visibility/public?searchtags=';
|
if (isPrivate) {
|
||||||
} else if (isPrivate) {
|
desiredVisibility = 'private';
|
||||||
url = basePath + '/admin/visibility/private';
|
|
||||||
} else if (isPublic) {
|
} else if (isPublic) {
|
||||||
url = basePath + '/admin/visibility/public';
|
desiredVisibility = 'public';
|
||||||
} else if (isUntagged) {
|
}
|
||||||
url = basePath + '/?searchtags=';
|
|
||||||
|
// 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);
|
console.log('[Filter] Navigating to:', url);
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear all filters and go to home
|
// Clear all filters - go back to showing all bookmarks
|
||||||
function clearAllFilters() {
|
function clearAllFilters() {
|
||||||
const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : '';
|
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
|
// Private and public are mutually exclusive
|
||||||
@ -455,24 +486,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
applyFilters();
|
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() {
|
(function initFilterStates() {
|
||||||
console.log('[Filter Debug] ========================================');
|
console.log('[Filter Debug] ========================================');
|
||||||
|
|
||||||
// Detect filter state from current URL path
|
// Get filter state from server-side rendered variables
|
||||||
const currentPath = window.location.pathname;
|
const visibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : '';
|
||||||
const currentSearch = window.location.search;
|
const untaggedonly = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) ? shaarli.untaggedonly : false;
|
||||||
|
|
||||||
// Detect visibility filter from URL path
|
// Detect active filters from server variables
|
||||||
let isPrivateActive = currentPath.includes('/visibility/private') || currentPath.includes('/admin/visibility/private');
|
let isPrivateActive = visibility === 'private';
|
||||||
let isPublicActive = currentPath.includes('/visibility/public') || currentPath.includes('/admin/visibility/public');
|
let isPublicActive = visibility === 'public';
|
||||||
|
let isUntaggedActive = untaggedonly === true;
|
||||||
|
|
||||||
// Detect untagged filter
|
console.log('[Filter Debug] shaarli.visibility:', visibility);
|
||||||
let isUntaggedActive = currentPath.includes('/untagged-only') ||
|
console.log('[Filter Debug] shaarli.untaggedonly:', untaggedonly);
|
||||||
(currentSearch.includes('searchtags=') && !currentSearch.match(/searchtags=[^&]+/));
|
|
||||||
|
|
||||||
console.log('[Filter Debug] URL path:', currentPath);
|
|
||||||
console.log('[Filter Debug] URL search:', currentSearch);
|
|
||||||
|
|
||||||
console.log('[Filter Debug] isPrivateActive:', isPrivateActive);
|
console.log('[Filter Debug] isPrivateActive:', isPrivateActive);
|
||||||
console.log('[Filter Debug] isPublicActive:', isPublicActive);
|
console.log('[Filter Debug] isPublicActive:', isPublicActive);
|
||||||
@ -512,45 +540,66 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Create and display the filter info banner
|
// Create and display the filter info banner
|
||||||
if (hasActiveFilter && !document.getElementById('filter-info-banner')) {
|
if (hasActiveFilter && !document.getElementById('filter-info-banner')) {
|
||||||
// Get result count from page
|
// Get result count from page - try multiple sources
|
||||||
let resultCount = '';
|
let resultCount = 0;
|
||||||
const pagingStats = document.querySelector('.paging-stats strong:last-child');
|
const pagingStats = document.querySelector('.paging-stats strong:last-child');
|
||||||
|
const linkCount = document.querySelectorAll('.link-outer').length;
|
||||||
|
|
||||||
if (pagingStats) {
|
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"
|
// Check if no results (empty state)
|
||||||
let filterParts = [];
|
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) {
|
if (isPrivateActive) {
|
||||||
filterParts.push('with status <strong>private</strong>');
|
statusPart = '<strong>private</strong>';
|
||||||
} else if (isPublicActive) {
|
} else if (isPublicActive) {
|
||||||
filterParts.push('with status <strong>public</strong>');
|
statusPart = '<strong>public</strong>';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUntaggedActive) {
|
if (isUntaggedActive) {
|
||||||
if (filterParts.length > 0) {
|
untaggedPart = '<strong>without any tag</strong>';
|
||||||
filterParts.push('and <strong>untagged</strong>');
|
}
|
||||||
} else {
|
|
||||||
filterParts.push('<strong>untagged</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] resultCount:', resultCount);
|
||||||
|
console.log('[Filter Debug] isEmptyResults:', isEmptyResults);
|
||||||
console.log('[Filter Debug] filterParts:', filterParts);
|
|
||||||
console.log('[Filter Debug] message:', message);
|
console.log('[Filter Debug] message:', message);
|
||||||
|
|
||||||
if (filterParts.length > 0) {
|
if (message) {
|
||||||
// Get base path safely
|
// Get base path safely
|
||||||
const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : '';
|
const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : '';
|
||||||
|
|
||||||
const banner = document.createElement('div');
|
const banner = document.createElement('div');
|
||||||
banner.id = 'filter-info-banner';
|
banner.id = 'filter-info-banner';
|
||||||
banner.className = 'filter-info-banner';
|
banner.className = 'filter-info-banner' + (isEmptyResults ? ' empty-results' : '');
|
||||||
banner.innerHTML = `
|
banner.innerHTML = `
|
||||||
<div class="filter-info-content">
|
<div class="filter-info-content">
|
||||||
<i class="mdi mdi-filter-variant"></i>
|
|
||||||
<span>${message}</span>
|
<span>${message}</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="filter-clear-btn" id="filter-clear-btn" title="Clear filters">
|
<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
|
// Try to find the best insertion point
|
||||||
const contentToolbar = document.querySelector('.content-toolbar');
|
const contentToolbar = document.querySelector('.content-toolbar');
|
||||||
const linklist = document.getElementById('linklist');
|
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] contentToolbar:', contentToolbar);
|
||||||
console.log('[Filter Debug] linklist:', linklist);
|
console.log('[Filter Debug] linklist:', linklist);
|
||||||
console.log('[Filter Debug] linksList:', linksList);
|
|
||||||
|
|
||||||
// Insert after the content-toolbar (pagination)
|
// Insert after the content-toolbar (pagination)
|
||||||
if (contentToolbar && contentToolbar.parentNode) {
|
if (contentToolbar && contentToolbar.parentNode) {
|
||||||
contentToolbar.parentNode.insertBefore(banner, contentToolbar.nextSibling);
|
contentToolbar.parentNode.insertBefore(banner, contentToolbar.nextSibling);
|
||||||
console.log('[Filter Debug] Banner inserted after content-toolbar');
|
console.log('[Filter Debug] Banner inserted after content-toolbar');
|
||||||
} else if (linksList && linksList.parentNode) {
|
} else if (emptyStateDiv && emptyStateDiv.parentNode) {
|
||||||
linksList.parentNode.insertBefore(banner, linksList);
|
emptyStateDiv.parentNode.insertBefore(banner, emptyStateDiv);
|
||||||
console.log('[Filter Debug] Banner inserted before links-list');
|
console.log('[Filter Debug] Banner inserted before empty-state');
|
||||||
} else if (linklist) {
|
} else if (linklist) {
|
||||||
linklist.insertBefore(banner, linklist.firstChild);
|
linklist.insertBefore(banner, linklist.firstChild);
|
||||||
console.log('[Filter Debug] Banner inserted at beginning of linklist');
|
console.log('[Filter Debug] Banner inserted at beginning of linklist');
|
||||||
@ -584,8 +632,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Add click handler to clear button
|
// Add click handler to clear button
|
||||||
document.getElementById('filter-clear-btn')?.addEventListener('click', () => {
|
document.getElementById('filter-clear-btn')?.addEventListener('click', () => {
|
||||||
console.log('[Filter] Clear button clicked, navigating to home');
|
console.log('[Filter] Clear button clicked, clearing all filters');
|
||||||
window.location.href = basePath + '/';
|
// 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