feat: améliorer styles dark mode avec background unifié #20293a, bordures notes/cards (1px solid #000 light, #dedfe2 dark), width input 600px→800px, ajout bouton pin corner absolu (top-right, hover scale 1.06), suppression backgrounds/borders/outlines inputs (transparent + !important), nettoyage code dupliqué note-input-collapsed, styles todo-draft-row transparent, et corrections mineures formatage CSS (espaces, indentation)

This commit is contained in:
Bruno Charest 2026-04-19 21:46:47 -04:00
parent ef6f9cb486
commit f99c1cbecb
11 changed files with 1098 additions and 200 deletions

View File

@ -19,7 +19,7 @@ body.view-todo .content-container {
}
[data-theme="dark"] body.view-todo .content-container {
background-color: var(--bg-body);
background-color: #20293a;
}
body.view-todo #linklist {
@ -105,7 +105,7 @@ body.view-todo #linklist {
}
[data-theme="dark"] .todo-main {
background-color: #0f172a;
background-color: #20293a;
}
.todo-main-header {
@ -212,14 +212,16 @@ body.view-todo #linklist {
/* --- NOTES VIEW --- */
/* Wrapper */
body.view-notes .content-container {
body.view-notes .content-container,
body.view-archive .content-container {
padding: 2rem;
background-color: var(--bg-body);
min-height: 100vh;
}
[data-theme="dark"] body.view-notes .content-container {
background-color: var(--bg-body);
[data-theme="dark"] body.view-notes .content-container,
[data-theme="dark"] body.view-archive .content-container {
background-color: #20293a;
}
/* Tool Bar / Input Area */
@ -233,7 +235,7 @@ body.view-notes .content-container {
}
.notes-top-bar-inner {
width: 600px;
width: 800px;
max-width: 100%;
display: flex;
align-items: flex-start;
@ -241,10 +243,11 @@ body.view-notes .content-container {
}
.note-input-container {
width: 600px;
width: 800px;
max-width: 100%;
background-color: var(--background-secondary, #ffffff);
border-radius: 8px;
border: 1px solid #000;
box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 2px 6px 2px rgba(60, 64, 67, 0.15);
transition: box-shadow 0.2s;
overflow: hidden;
@ -255,9 +258,9 @@ body.view-notes .content-container {
}
[data-theme="dark"] .note-input-container {
background-color: #202124;
border: none;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.6), 0 2px 6px 2px rgba(0, 0, 0, 0.4);
background-color: #20293a;
border: 1px solid var(--border);
box-shadow: none;
}
.note-input-collapsed {
@ -268,6 +271,16 @@ body.view-notes .content-container {
color: var(--note-card-fg, var(--text-light, #80868b));
font-weight: 500;
font-size: 1rem;
padding: 12px 16px;
cursor: text;
color: var(--note-card-fg, var(--text-light, #80868b));
font-weight: 500;
font-size: 1rem;
padding: 12px 16px;
cursor: text;
color: var(--note-card-fg, var(--text-light, #80868b));
font-weight: 500;
font-size: 1rem;
}
.note-input-container.is-editing {
@ -327,6 +340,14 @@ body.view-notes .content-container {
.todo-draft-row .todo-item-text {
padding: 5px 4px;
font-size: 0.95rem;
background-color: transparent;
border: none !important;
outline: none !important;
box-shadow: none !important;
}
[data-theme="dark"] .todo-draft-row .todo-item-text {
background-color: transparent;
}
.todo-draft-add-btn {
@ -355,26 +376,32 @@ body.view-notes .content-container {
.note-input-title {
width: 100%;
border: 0;
outline: none;
background: transparent;
border: none !important;
outline: none !important;
box-shadow: none !important;
background-color: transparent;
color: inherit;
font-size: 1.05rem;
font-weight: 500;
padding: 0;
}
[data-theme="dark"] .note-input-title {
background-color: transparent;
}
.note-input-description-source {
width: 100%;
border: none;
background: transparent;
border: none !important;
outline: none !important;
box-shadow: none !important;
background-color: transparent;
color: inherit;
padding: 0;
resize: none;
min-height: 120px;
max-height: 44vh;
overflow: auto;
outline: none;
display: block;
font-size: 0.95rem;
line-height: 1.5;
@ -385,7 +412,8 @@ body.view-notes .content-container {
}
[data-theme="dark"] .note-input-description-source {
border: none;
border: none !important;
background-color: transparent;
}
.note-input-container.is-enhanced .note-input-description-source {
@ -419,8 +447,15 @@ body.view-notes .content-container {
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.note-format-btn {
@ -619,15 +654,63 @@ body.view-notes .content-container {
column-span: none;
border-radius: 12px;
overflow: hidden;
border: 1px solid #000;
}
.note-card {
border-radius: 12px;
overflow: hidden;
border: 1px solid #000;
}
[data-theme="dark"] .note-card {
border-color: #dedfe2;
}
.note-card .note-inner {
position: relative;
padding: 14px 16px 12px;
padding-right: 58px;
}
.note-pin-corner {
position: absolute;
top: 10px;
right: 10px;
width: auto;
height: auto;
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
color: var(--note-card-fg, currentColor);
background: transparent;
border: none;
z-index: 4;
opacity: 0.92;
transition: transform 0.15s ease, opacity 0.15s ease, color 0.15s ease;
}
.note-pin-corner i {
font-size: 1.35rem;
}
.note-pin-corner:hover {
transform: scale(1.06);
opacity: 1;
}
.note-pin-corner.active {
color: var(--note-card-fg, currentColor);
}
[data-theme="dark"] .note-pin-corner {
background: transparent;
border: none;
}
[data-theme="dark"] .note-pin-corner.active {
color: var(--note-card-fg, currentColor);
}
@media (max-width: 1200px) {
@ -683,7 +766,7 @@ body.view-notes .content-container {
overflow: hidden;
border-radius: 8px;
background: var(--background-secondary, #ffffff);
border: 1px solid rgba(0, 0, 0, 0.12);
border: 1px solid #000;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
color: inherit;
display: flex;
@ -696,7 +779,7 @@ body.note-modal-open {
[data-theme="dark"] .note-modal {
background: #202124;
border-color: transparent;
border-color: #dedfe2;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.5), 0 10px 10px rgba(0, 0, 0, 0.4);
}
@ -1740,14 +1823,14 @@ body.note-modal-open {
/* Reference: Keep Colors */
/* Default */
.note-card.note-color-default {
background-color: #20293a;
border-color: transparent;
--note-card-fg: #dbe7ff;
background-color: var(--bg-body);
border-color: #000;
--note-card-fg: var(--text-main, #111827);
}
[data-theme="dark"] .note-card.note-color-default {
background-color: #20293a;
border-color: transparent;
border-color: #dedfe2;
--note-card-fg: #dbe7ff;
}
@ -1916,6 +1999,11 @@ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
.note-card[class*="note-color-"] {
color: var(--note-card-fg, #202124);
border: 1px solid #000;
}
[data-theme="dark"] .note-card[class*="note-color-"] {
border: 1px solid #dedfe2;
}
.link-outer {
@ -1994,6 +2082,7 @@ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
/* Responsive: sur petits écrans, ajuster les backgrounds */
@media (max-width: 768px) {
/* En mobile, les cards sont plus étroites, on ajuste */
.notes-masonry .note-card.note-has-bg,
.view-grid .link-outer.note-has-bg {
@ -2052,21 +2141,17 @@ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
.note-card.note-filter-lined,
.note-modal.note-filter-lined,
.link-outer.note-filter-lined {
background-image: repeating-linear-gradient(
transparent,
background-image: repeating-linear-gradient(transparent,
transparent 29px,
rgba(0, 0, 0, 0.1) 30px
) !important;
rgba(0, 0, 0, 0.1) 30px) !important;
}
[data-theme="dark"] .note-card.note-filter-lined,
[data-theme="dark"] .note-modal.note-filter-lined,
[data-theme="dark"] .link-outer.note-filter-lined {
background-image: repeating-linear-gradient(
transparent,
background-image: repeating-linear-gradient(transparent,
transparent 29px,
rgba(255, 255, 255, 0.1) 30px
) !important;
rgba(255, 255, 255, 0.1) 30px) !important;
}
/* 4. Quadrillé - Grid */
@ -2126,25 +2211,21 @@ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
.note-card.note-filter-stripes,
.note-modal.note-filter-stripes,
.link-outer.note-filter-stripes {
background-image: repeating-linear-gradient(
45deg,
background-image: repeating-linear-gradient(45deg,
transparent,
transparent 10px,
rgba(0, 0, 0, 0.05) 10px,
rgba(0, 0, 0, 0.05) 20px
) !important;
rgba(0, 0, 0, 0.05) 20px) !important;
}
[data-theme="dark"] .note-card.note-filter-stripes,
[data-theme="dark"] .note-modal.note-filter-stripes,
[data-theme="dark"] .link-outer.note-filter-stripes {
background-image: repeating-linear-gradient(
45deg,
background-image: repeating-linear-gradient(45deg,
transparent,
transparent 10px,
rgba(255, 255, 255, 0.05) 10px,
rgba(255, 255, 255, 0.05) 20px
) !important;
rgba(255, 255, 255, 0.05) 20px) !important;
}
/* =========================================
@ -2521,7 +2602,12 @@ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
/* Custom note color style */
.note-card.note-color-custom,
.note-modal.note-color-custom {
border-color: transparent;
border-color: #000;
}
[data-theme="dark"] .note-card.note-color-custom,
[data-theme="dark"] .note-modal.note-color-custom {
border-color: #dedfe2;
}
/* Ensure Font section is properly sized */
@ -2533,14 +2619,13 @@ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
}
/* --- ARCHIVE VIEW --- */
/* Ensure archive view uses same background as notes view */
body.view-archive .content-container {
padding: 2rem;
background-color: var(--bg-body);
min-height: 100vh;
background-color: var(--bg-body) !important;
}
[data-theme="dark"] body.view-archive .content-container {
background-color: var(--bg-body);
background-color: #20293a !important;
}
/* Archive Title */
@ -2579,25 +2664,20 @@ body.view-archive .content-container {
color: #9aa0a6;
}
/* Archive wrapper */
.archive-wrapper {
max-width: 1200px;
margin: 0 auto;
}
/* Archive top bar adjustments */
.archive-top-bar {
flex-direction: column;
align-items: center;
padding-right: 0;
gap: 1rem;
position: relative;
}
.archive-top-bar .notes-tools {
position: relative;
right: auto;
top: auto;
transform: none;
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
}
body.view-todo .note-card.todo-card .note-body {

View File

@ -325,6 +325,7 @@ a:focus:not(:focus-visible) {
.sidebar-add-segment span {
display: none;
}
.sidebar-add-segment {
padding: 0.75rem;
}
@ -3246,6 +3247,7 @@ select:focus {
.page-edit .toastui-editor-mode-switch {
background: var(--bookmark-input-bg);
}
.page-edit .toastui-editor-defaultUI {
border: 0;
}
@ -3266,6 +3268,10 @@ select:focus {
background-color: rgba(126, 168, 255, 0.16);
}
.page-edit .toastui-editor-toolbar-margin {
margin-bottom: 0.5rem;
}
.page-edit .toastui-editor-toolbar-icons {
opacity: 0.96;
}
@ -3283,6 +3289,148 @@ select:focus {
border-top: 1px solid var(--bookmark-input-border);
}
[data-theme="dark"] .page-add .toastui-editor-defaultUI,
[data-theme="dark"] .page-add .toastui-editor-md-container,
[data-theme="dark"] .page-add .toastui-editor-ww-container,
[data-theme="dark"] .page-edit .toastui-editor-defaultUI,
[data-theme="dark"] .page-edit .toastui-editor-md-container,
[data-theme="dark"] .page-edit .toastui-editor-ww-container {
background: var(--toastui-panel-surface, #0a1429) !important;
color: var(--bookmark-text-main, var(--text-main)) !important;
border-color: var(--toastui-border, #1f3560) !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar,
[data-theme="dark"] .page-add .toastui-editor-mode-switch,
[data-theme="dark"] .page-edit .toastui-editor-toolbar,
[data-theme="dark"] .page-edit .toastui-editor-mode-switch {
background: var(--toastui-toolbar-bg, linear-gradient(180deg, rgba(28, 49, 86, 0.95) 0%, rgba(14, 26, 47, 0.96) 100%)) !important;
border-color: var(--toastui-border, #1f3560) !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar,
[data-theme="dark"] .page-edit .toastui-editor-toolbar {
border-bottom: 1px solid var(--toastui-border, #1f3560) !important;
box-shadow: inset 0 -1px 0 rgba(9, 12, 23, 0.6) !important;
}
[data-theme="dark"] .page-add .toastui-editor-md-tab-container,
[data-theme="dark"] .page-add .toastui-editor-mode-switch,
[data-theme="dark"] .page-edit .toastui-editor-md-tab-container,
[data-theme="dark"] .page-edit .toastui-editor-mode-switch {
border-top: 1px solid var(--toastui-border, #1f3560) !important;
}
[data-theme="dark"] .page-add .toastui-editor-mode-switch .tab-item,
[data-theme="dark"] .page-edit .toastui-editor-mode-switch .tab-item {
background: transparent !important;
color: var(--bookmark-text-muted, var(--text-secondary)) !important;
border-radius: 6px;
padding: 0.45rem 0.8rem;
transition: background-color 0.2s ease, color 0.2s ease;
}
[data-theme="dark"] .page-add .toastui-editor-mode-switch .tab-item.active,
[data-theme="dark"] .page-add .toastui-editor-mode-switch .tab-item.selected,
[data-theme="dark"] .page-edit .toastui-editor-mode-switch .tab-item.active,
[data-theme="dark"] .page-edit .toastui-editor-mode-switch .tab-item.selected {
background: var(--toastui-tab-hover, rgba(112, 160, 255, 0.24)) !important;
color: var(--bookmark-text-main, var(--text-main)) !important;
box-shadow: 0 4px 18px rgba(23, 40, 72, 0.55);
}
[data-theme="dark"] .page-add .toastui-editor-toolbar button,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button {
color: #e2e9ff !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar button:hover,
[data-theme="dark"] .page-add .toastui-editor-toolbar button.active,
[data-theme="dark"] .page-add .toastui-editor-toolbar button:focus-visible,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button:hover,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button.active,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button:focus-visible {
background-color: rgba(118, 164, 255, 0.26) !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar button:hover .toastui-editor-toolbar-icons,
[data-theme="dark"] .page-add .toastui-editor-toolbar button.active .toastui-editor-toolbar-icons,
[data-theme="dark"] .page-add .toastui-editor-toolbar button:focus-visible .toastui-editor-toolbar-icons,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button:hover .toastui-editor-toolbar-icons,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button.active .toastui-editor-toolbar-icons,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button:focus-visible .toastui-editor-toolbar-icons {
opacity: 1 !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar-icons,
[data-theme="dark"] .page-edit .toastui-editor-toolbar-icons {
opacity: 0.85 !important;
filter: none !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar-icons::before,
[data-theme="dark"] .page-add .toastui-editor-toolbar-icons::after,
[data-theme="dark"] .page-edit .toastui-editor-toolbar-icons::before,
[data-theme="dark"] .page-edit .toastui-editor-toolbar-icons::after {
filter: none !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar button.toastui-editor-toolbar-icons,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button.toastui-editor-toolbar-icons {
filter: none !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar button svg,
[data-theme="dark"] .page-add .toastui-editor-toolbar button svg *,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button svg,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button svg * {
fill: rgba(255, 255, 255, 0.85) !important;
stroke: rgba(255, 255, 255, 0.85) !important;
opacity: 1 !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar-divider,
[data-theme="dark"] .page-edit .toastui-editor-toolbar-divider {
background-color: rgba(255, 255, 255, 0.12) !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar *,
[data-theme="dark"] .page-edit .toastui-editor-toolbar * {
background-color: transparent;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar-group,
[data-theme="dark"] .page-edit .toastui-editor-toolbar-group {
background: transparent !important;
border-color: rgba(255, 255, 255, 0.12) !important;
box-shadow: none !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar-group:not(:last-child),
[data-theme="dark"] .page-edit .toastui-editor-toolbar-group:not(:last-child) {
border-right-color: rgba(255, 255, 255, 0.12) !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar-item,
[data-theme="dark"] .page-edit .toastui-editor-toolbar-item {
background-color: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
}
[data-theme="dark"] .page-add .toastui-editor-toolbar button,
[data-theme="dark"] .page-edit .toastui-editor-toolbar button {
background-color: transparent !important;
border-color: transparent !important;
}
[data-theme="dark"] .page-add .toastui-editor-contents,
[data-theme="dark"] .page-add .toastui-editor-contents *,
[data-theme="dark"] .page-edit .toastui-editor-contents,
[data-theme="dark"] .page-edit .toastui-editor-contents * {
color: var(--bookmark-text-main, var(--text-main)) !important;
}
.page-edit .bookmark-tags-input {
min-height: 48px;
display: flex;
@ -3534,6 +3682,11 @@ select:focus {
--bookmark-tag-remove-bg: rgba(255, 255, 255, 0.16);
--bookmark-tag-remove-bg-hover: rgba(255, 255, 255, 0.26);
--bookmark-save-bg: linear-gradient(135deg, #6fa8ff 0%, #4d82f0 100%);
--toastui-panel-bg: #101d38;
--toastui-panel-surface: #0a1429;
--toastui-toolbar-bg: linear-gradient(180deg, rgba(28, 49, 86, 0.95) 0%, rgba(14, 26, 47, 0.96) 100%);
--toastui-border: #1f3560;
--toastui-tab-hover: rgba(112, 160, 255, 0.24);
}
.page-edit .bookmark-editor-card {

View File

@ -17,10 +17,10 @@
{/if}
{$batchModeValue=empty($batch_mode) ? '0' : '1'}
{function="($readLaterChecked = strpos(' ' . $link.tags . ' ', ' readitlater ') != false || strpos(' ' . $link.tags . ' ', ' readlater ') != false || strpos(' ' . $link.tags . ' ', ' toread ') != false) ? '' : ''"}
{function="($isNoteOrTodo = strpos(' ' . $link.tags . ' ', ' note ') != false || strpos(' ' . $link.tags . ' ', ' shaarli-todo ') != false || strpos(' ' . $link.tags . ' ', ' shaarli-todo ') != false) ? '' : ''"}
{function="($isNoteOrTodo = strpos(' ' . $link.tags . ' ', ' note ') != false || strpos(' ' . $link.tags . ' ', ' shaarli-note ') != false || strpos(' ' . $link.tags . ' ', ' todo ') != false || strpos(' ' . $link.tags . ' ', ' shaarli-todo ') != false) ? '' : ''"}
{function="($noteDefaultChecked = $link_is_new && empty($link.url) && !$isNoteOrTodo) ? '' : ''"}
{function="($noteChecked = strpos(' ' . $link.tags . ' ', ' note ') != false || $noteDefaultChecked) ? '' : ''"}
{function="($todoChecked = strpos(' ' . $link.tags . ' ', ' shaarli-todo ') != false) ? '' : ''"}
{function="($noteChecked = strpos(' ' . $link.tags . ' ', ' note ') != false || strpos(' ' . $link.tags . ' ', ' shaarli-note ') != false || $noteDefaultChecked) ? '' : ''"}
{function="($todoChecked = strpos(' ' . $link.tags . ' ', ' todo ') != false || strpos(' ' . $link.tags . ' ', ' shaarli-todo ') != false) ? '' : ''"}
{$effectiveTags=$link.tags}
{if="$noteDefaultChecked && strpos(' ' . $effectiveTags . ' ', ' note ') == false"}
{$effectiveTags=trim($effectiveTags . ' note')}

View File

@ -67,6 +67,7 @@ visibility: '{$visibility}',
untaggedonly: (function(){/*{if="$untaggedonly"}*/return true;/*{else}*/return false;/*{/if}*/})()
};
</script>
<script src="{$base_path}/{function="ltrim($asset_path, '/')"}/js/shaarit-rules.js?v=1.0.0" defer></script>
<script src="{$base_path}/{function="ltrim($asset_path, '/')"}/js/script.js?v=1.0.5" defer></script>
<script src="{$base_path}/{function="ltrim($asset_path, '/')"}/js/custom_views.js?v=1.0.6" defer></script>

View File

@ -13,14 +13,18 @@ document.addEventListener("DOMContentLoaded", function () {
// Foolproof detection using sidebar active state and DOM rendered tags
const hasNoteActiveMenu = !!document.querySelector('.sidebar-link[aria-label="Notes"].active, .header-nav-link[aria-label="Notes"].active, .sidebar-link[href*="searchtags=note"].active, .sidebar-link[href*="searchtags=shaarli-note"].active');
const hasTodoActiveMenu = !!document.querySelector('.sidebar-link[aria-label="Mes tâches"].active, .header-nav-link[aria-label="Mes tâches"].active, .sidebar-link[href*="searchtags=shaarli-todo"].active');
const hasArchiveActiveMenu = !!document.querySelector('.sidebar-link[aria-label="Archive"].active, .header-nav-link[aria-label="Archive"].active, .sidebar-link[href*="searchtags=shaarli-archive"].active');
const hasTodoActiveMenu = !!document.querySelector('.sidebar-link[aria-label="Mes tâches"].active, .header-nav-link[aria-label="Mes tâches"].active, .sidebar-link[href*="searchtags=shaarli-todo"].active, .sidebar-link[href*="searchtags=todo"].active');
const hasArchiveActiveMenu = !!document.querySelector('.sidebar-link[aria-label="Archive"].active, .header-nav-link[aria-label="Archive"].active, .sidebar-link[href*="searchtags=shaarli-archive"].active, .sidebar-link[href*="searchtags=shaarli-archiver"].active');
const domChipTags = Array.from(document.querySelectorAll('.search-tag-chip')).map(el => (el.textContent || "").trim().toLowerCase());
const isNoteView = activeTags.includes("note") || activeTags.includes("shaarli-note") || hasNoteActiveMenu || domChipTags.includes("note") || domChipTags.includes("shaarli-note");
const isTodoView = activeTags.includes("shaarli-todo") || hasTodoActiveMenu || domChipTags.includes("shaarli-todo");
const isArchiveView = activeTags.includes("shaarli-archive") || hasArchiveActiveMenu || domChipTags.includes("shaarli-archive");
const isArchiveTag = (tag) => tag === "shaarli-archive" || tag === "shaarli-archiver";
const isNoteTag = (tag) => tag === "note" || tag === "shaarli-note" || tag === "#note";
const isTodoTag = (tag) => tag === "todo" || tag === "shaarli-todo" || tag === "#todo";
const isNoteView = activeTags.some(isNoteTag) || hasNoteActiveMenu || domChipTags.some(isNoteTag);
const isTodoView = activeTags.some(isTodoTag) || hasTodoActiveMenu || domChipTags.some(isTodoTag);
const isArchiveView = activeTags.some(isArchiveTag) || hasArchiveActiveMenu || domChipTags.some(isArchiveTag);
const linkList = document.getElementById("links-list");
const container = document.querySelector(".content-container");
@ -500,7 +504,7 @@ const NOTE_COLOR_OPTIONS = [
{
key: "default",
label: "Par défaut",
light: "#ffffff",
light: "#f8fafc",
dark: "#20293A"
},
{
@ -605,13 +609,15 @@ const NOTE_BACKGROUND_TAG_PREFIX = "notebg-";
function isTechnicalTag(tag) {
if (typeof tag !== "string") return false;
const t = tag.trim();
const t = tag.trim().toLowerCase();
if (!t) return false;
if (t === "note") return true;
if (t === "note" || t === "#note") return true;
if (t === "shaarli-note") return true;
if (t === "todo" || t === "#todo") return true;
if (t === "shaarli-todo") return true;
if (t === "shaarli-pin") return true;
if (t === "brain-dump") return true;
if (t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)) return true;
if (t.startsWith(NOTE_COLOR_TAG_PREFIX)) return true;
if (t.startsWith(NOTE_FILTER_TAG_PREFIX)) return true;
@ -694,6 +700,16 @@ function removeTagFromEntity(editUrl, tag) {
});
}
function deleteEntitySilently(deleteUrl) {
if (!deleteUrl || deleteUrl === "#") return Promise.reject("Invalid delete URL");
return fetch(deleteUrl)
.then((response) => {
if (!response.ok) throw new Error("Delete request failed");
return response;
});
}
let tagDisplayRemovalInitialized = false;
function initTagDisplayAndRemoval() {
if (tagDisplayRemovalInitialized) return;
@ -1136,7 +1152,7 @@ function getElementVisualFontColor(element) {
}
function refreshNoteFilterVisuals() {
document.querySelectorAll(".note-card, .note-modal, .link-outer").forEach((element) => {
document.querySelectorAll(".note-card, .note-modal, .link-outer, .note-input-container").forEach((element) => {
applyNoteVisualState(element, {
color: getElementVisualColor(element),
filter: getElementVisualFilter(element),
@ -1238,7 +1254,7 @@ function initThemeModeBackgroundSync() {
if (nextTheme === lastTheme) return;
lastTheme = nextTheme;
refreshNoteBackgroundVisuals();
refreshNoteFilterVisuals();
refreshBackgroundPalettes();
});
@ -1388,17 +1404,12 @@ function ensureBackgroundStudioPanel() {
if (action === "set-defaults") {
if (mode === "draft") {
panelEl.dataset.color = "default";
panelEl.dataset.filter = "none";
panelEl.dataset.background = "none";
panelEl.dataset.fontColor = "auto";
applyDraft({ color: "default", filter: "none", background: "none", fontColor: "auto" });
applyDraft({ color: "default" });
renderBackgroundStudioPanel(panelEl);
} else if (mode === "modal") {
setModalNoteColor("default");
setModalNoteFilter("none");
} else {
setNoteColor(entityId, "default", editUrl);
setNoteFilter(entityId, "none", editUrl);
}
return;
}
@ -1814,7 +1825,7 @@ function applyNoteVisualState(element, note) {
if (colorValue) {
element.style.backgroundColor = colorValue;
element.style.borderColor = "transparent";
element.style.removeProperty("border-color");
} else {
element.style.removeProperty("background-color");
}
@ -2323,7 +2334,7 @@ function initTodoView(linkList, container) {
<div class="palette-popup note-modal-palette" id="todo-modal-color-popup"></div>
</div>
<a href="#" id="todo-modal-edit" title="Modifier"><i class="mdi mdi-pencil-outline"></i></a>
<button type="button" id="todo-modal-delete" title="Supprimer"><i class="mdi mdi-dots-vertical"></i></button>
<button type="button" id="todo-modal-delete" title="Supprimer"><i class="mdi mdi-delete-outline"></i></button>
</div>
<button type="button" class="note-modal-close-btn" id="todo-modal-close">Fermer</button>
</div>
@ -2403,10 +2414,19 @@ function initTodoView(linkList, container) {
});
modalOverlay.querySelector("#todo-modal-delete").addEventListener("click", () => {
if (!confirm("Supprimer cette tâche ?")) return;
const modalCard = modalOverlay.querySelector(".note-modal");
const deleteUrl = modalCard.dataset.deleteUrl;
const todoId = modalCard.dataset.todoId;
if (deleteUrl && deleteUrl !== "#") {
window.location.href = deleteUrl;
deleteEntitySilently(deleteUrl)
.then(() => {
window.location.href = "/?searchtags=shaarli-todo";
})
.catch((err) => {
console.error("Delete failed:", err);
alert("Erreur lors de la suppression.");
});
}
});
@ -2884,6 +2904,13 @@ function renderTodos(container, todos, viewMode) {
const actions = document.createElement("div");
actions.className = "note-hover-actions";
const pinCorner = document.createElement("a");
pinCorner.href = todo.pinUrl;
pinCorner.title = todo.isPinned ? "Unpin" : "Pin";
pinCorner.className = `note-pin-corner ${todo.isPinned ? "active" : ""}`;
pinCorner.innerHTML = `<i class="mdi mdi-pin${todo.isPinned ? "" : "-outline"}"></i>`;
inner.appendChild(pinCorner);
const paletteBtnId = `palette-${todo.id}`;
actions.innerHTML = `
@ -2891,11 +2918,25 @@ function renderTodos(container, todos, viewMode) {
<button title="Couleur" id="${paletteBtnId}"><i class="mdi mdi-palette-outline"></i></button>
</div>
<div class="spacer"></div>
<a href="${todo.pinUrl}" title="${todo.isPinned ? "Unpin" : "Pin"}" class="${todo.isPinned ? "active" : ""}"><i class="mdi mdi-pin${todo.isPinned ? "" : "-outline"}"></i></a>
<a href="${todo.editUrl}" title="Edit"><i class="mdi mdi-pencil-outline"></i></a>
<button title="Plus" onclick="window.location.href='${todo.deleteUrl}'"><i class="mdi mdi-dots-vertical"></i></button>
<a href="${todo.editUrl}" title="Modifier"><i class="mdi mdi-pencil-outline"></i></a>
<button title="Supprimer" class="todo-delete-btn"><i class="mdi mdi-delete-outline"></i></button>
`;
const deleteBtn = actions.querySelector(".todo-delete-btn");
deleteBtn?.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (!confirm("Supprimer cette tâche ?")) return;
deleteEntitySilently(todo.deleteUrl)
.then(() => {
window.location.href = "/?searchtags=shaarli-todo";
})
.catch((err) => {
console.error("Delete failed:", err);
alert("Erreur lors de la suppression.");
});
});
const paletteBtn = actions.querySelector(`#${paletteBtnId}`);
paletteBtn.addEventListener("click", (e) => {
e.preventDefault();
@ -3841,7 +3882,7 @@ function initNoteView(linkList, container) {
<button type="button" title="Image"><i class="mdi mdi-image-outline"></i></button>
<button type="button" id="note-modal-archive" title="Archiver"><i class="mdi mdi-archive-arrow-down-outline"></i></button>
<a href="#" id="note-modal-edit" title="Modifier"><i class="mdi mdi-pencil-outline"></i></a>
<button type="button" id="note-modal-delete" title="Supprimer"><i class="mdi mdi-dots-vertical"></i></button>
<button type="button" id="note-modal-delete" title="Supprimer"><i class="mdi mdi-delete-outline"></i></button>
</div>
<button type="button" class="note-modal-close-btn" id="note-modal-close">Fermer</button>
</div>
@ -3945,10 +3986,19 @@ function initNoteView(linkList, container) {
});
modalOverlay.querySelector("#note-modal-delete").addEventListener("click", () => {
if (!confirm("Supprimer cette note ?")) return;
const modalCard = modalOverlay.querySelector(".note-modal");
const deleteUrl = modalCard.dataset.deleteUrl;
const noteId = modalCard.dataset.noteId;
if (deleteUrl && deleteUrl !== "#") {
window.location.href = deleteUrl;
deleteEntitySilently(deleteUrl)
.then(() => {
window.location.href = "/?searchtags=note";
})
.catch((err) => {
console.error("Delete failed:", err);
alert("Erreur lors de la suppression.");
});
}
});
@ -4046,7 +4096,7 @@ function initArchiveView(linkList, container) {
const notes = links.map((link) => parseNoteFromLink(link));
// Filter only archived notes
const archivedNotes = notes.filter((note) => (note.tags || []).includes("shaarli-archive"));
const archivedNotes = notes.filter((note) => (note.tags || []).some((t) => t === "shaarli-archive" || t === "shaarli-archiver"));
// Initial Render (Grid)
renderNotes(contentArea, archivedNotes, "grid", true); // true = archive mode
@ -4085,7 +4135,7 @@ function initArchiveView(linkList, container) {
<button type="button" title="Image"><i class="mdi mdi-image-outline"></i></button>
<button type="button" id="note-modal-unarchive" title="Désarchiver"><i class="mdi mdi-archive-arrow-up-outline"></i></button>
<a href="#" id="note-modal-edit" title="Modifier"><i class="mdi mdi-pencil-outline"></i></a>
<button type="button" id="note-modal-delete" title="Supprimer"><i class="mdi mdi-dots-vertical"></i></button>
<button type="button" id="note-modal-delete" title="Supprimer"><i class="mdi mdi-delete-outline"></i></button>
</div>
<button type="button" class="note-modal-close-btn" id="note-modal-close">Fermer</button>
</div>
@ -4171,10 +4221,19 @@ function initArchiveView(linkList, container) {
});
modalOverlay.querySelector("#note-modal-delete").addEventListener("click", () => {
if (!confirm("Supprimer cette note ?")) return;
const modalCard = modalOverlay.querySelector(".note-modal");
const deleteUrl = modalCard.dataset.deleteUrl;
const noteId = modalCard.dataset.noteId;
if (deleteUrl && deleteUrl !== "#") {
window.location.href = deleteUrl;
deleteEntitySilently(deleteUrl)
.then(() => {
window.location.href = "/?searchtags=shaarli-archive";
})
.catch((err) => {
console.error("Delete failed:", err);
alert("Erreur lors de la suppression.");
});
}
});
@ -4293,10 +4352,10 @@ function renderNotes(container, notes, viewMode, isArchiveMode = false) {
let visibleNotes;
if (isArchiveMode) {
// In archive mode: show only notes with shaarli-archive tag
visibleNotes = notes.filter((note) => (note.tags || []).includes("shaarli-archive"));
visibleNotes = notes.filter((note) => (note.tags || []).some((t) => t === "shaarli-archive" || t === "shaarli-archiver"));
} else {
// In normal notes mode: hide archived notes
visibleNotes = notes.filter((note) => !(note.tags || []).includes("shaarli-archive"));
visibleNotes = notes.filter((note) => !(note.tags || []).some((t) => t === "shaarli-archive" || t === "shaarli-archiver"));
}
// Sort: Pinned items first
@ -4379,6 +4438,13 @@ function renderNotes(container, notes, viewMode, isArchiveMode = false) {
const actions = document.createElement("div");
actions.className = "note-hover-actions";
const pinCorner = document.createElement("a");
pinCorner.href = note.pinUrl;
pinCorner.title = note.isPinned ? "Unpin" : "Pin";
pinCorner.className = `note-pin-corner ${note.isPinned ? "active" : ""}`;
pinCorner.innerHTML = `<i class="mdi mdi-pin${note.isPinned ? "" : "-outline"}"></i>`;
inner.appendChild(pinCorner);
// Palette Button Logic
const paletteBtnId = `palette-${note.id}`;
const archiveBtnId = `archive-${note.id}`;
@ -4393,11 +4459,25 @@ function renderNotes(container, notes, viewMode, isArchiveMode = false) {
<button title="Archiver" id="${archiveBtnId}"><i class="mdi mdi-archive-arrow-down-outline"></i></button>
<div class="spacer"></div>
<!-- Real Actions -->
<a href="${note.pinUrl}" title="${note.isPinned ? "Unpin" : "Pin"}" class="${note.isPinned ? "active" : ""}"><i class="mdi mdi-pin${note.isPinned ? "" : "-outline"}"></i></a>
<button type="button" class="note-open-editor-btn" title="Modifier"><i class="mdi mdi-pencil-outline"></i></button>
<button title="Plus" onclick="window.location.href='${note.deleteUrl}'"><i class="mdi mdi-dots-vertical"></i></button>
<button title="Supprimer" class="note-delete-btn"><i class="mdi mdi-delete-outline"></i></button>
`;
const deleteBtn = actions.querySelector(".note-delete-btn");
deleteBtn?.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (!confirm("Supprimer cette note ?")) return;
deleteEntitySilently(note.deleteUrl)
.then(() => {
window.location.href = isArchiveMode ? "/?searchtags=shaarli-archive" : "/?searchtags=note";
})
.catch((err) => {
console.error("Delete failed:", err);
alert("Erreur lors de la suppression.");
});
});
const openEditorBtn = actions.querySelector(".note-open-editor-btn");
openEditorBtn?.addEventListener("click", (e) => {
e.preventDefault();
@ -5211,7 +5291,7 @@ function initPinnedItems() {
document.addEventListener(
"click",
function (e) {
const btn = e.target.closest('a[href*="do=pin"], .note-hover-actions a[href*="pin"], .link-actions a[href*="pin"]');
const btn = e.target.closest('a[href*="do=pin"], .note-hover-actions a[href*="pin"], .note-pin-corner[href*="pin"], .link-actions a[href*="pin"]');
if (btn) {
e.preventDefault();
e.stopPropagation();
@ -5732,10 +5812,11 @@ function openColorPickerPanel({ mode, entityId, editUrl, type }) {
setFromHex(defaultColor);
}
panel.style.display = "block";
panel.style.visibility = "hidden";
// Position the color picker panel
positionColorPickerPanel(panel, mode);
panel.style.display = "block";
panel.style.visibility = "visible";
panel.classList.add("open");
panel.setAttribute("aria-hidden", "false");
}
@ -5751,20 +5832,26 @@ function positionColorPickerPanel(panel, mode) {
if (bgPanel && bgPanel.classList.contains("open")) {
const bgRect = bgPanel.getBoundingClientRect();
const panelRect = panel.getBoundingClientRect();
const panelWidth = Math.max(1, panelRect.width);
const panelHeight = Math.max(1, panelRect.height);
// Position to the right of the bg-studio panel
let left = bgRect.right + 10;
let top = bgRect.top;
// Check if it fits on the right
if (left + panel.offsetWidth > window.innerWidth - viewportPadding) {
if (left + panelWidth > window.innerWidth - viewportPadding) {
// Position to the left instead
left = bgRect.left - panel.offsetWidth - 10;
left = bgRect.left - panelWidth - 10;
}
// Ensure it stays within viewport
if (left < viewportPadding) left = viewportPadding;
if (top + panel.offsetHeight > window.innerHeight - viewportPadding) {
top = window.innerHeight - viewportPadding - panel.offsetHeight;
if (left + panelWidth > window.innerWidth - viewportPadding) {
left = window.innerWidth - viewportPadding - panelWidth;
}
if (top + panelHeight > window.innerHeight - viewportPadding) {
top = window.innerHeight - viewportPadding - panelHeight;
}
if (top < viewportPadding) top = viewportPadding;
@ -5775,8 +5862,10 @@ function positionColorPickerPanel(panel, mode) {
} else {
// Center in viewport if bg panel not available
const panelRect = panel.getBoundingClientRect();
const left = Math.max(viewportPadding, (window.innerWidth - panelRect.width) / 2);
const top = Math.max(viewportPadding, (window.innerHeight - panelRect.height) / 2);
const clampedWidth = Math.max(1, panelRect.width);
const clampedHeight = Math.max(1, panelRect.height);
const left = Math.max(viewportPadding, Math.min((window.innerWidth - clampedWidth) / 2, window.innerWidth - viewportPadding - clampedWidth));
const top = Math.max(viewportPadding, Math.min((window.innerHeight - clampedHeight) / 2, window.innerHeight - viewportPadding - clampedHeight));
panel.style.left = `${Math.round(left)}px`;
panel.style.top = `${Math.round(top)}px`;
}
@ -5899,7 +5988,7 @@ function setNoteColorVisual(noteId, colorKey) {
});
element.classList.add("note-color-custom");
element.style.backgroundColor = hex;
element.style.borderColor = "transparent";
element.style.removeProperty("border-color");
element.dataset.color = "custom";
element.dataset.customColor = hex;
const fg = getReadableForegroundForBackground(hex);
@ -6038,7 +6127,7 @@ function setModalCustomNoteColor(color) {
const modalCard = modal.querySelector(".note-modal");
if (modalCard) {
modalCard.style.backgroundColor = color;
modalCard.style.borderColor = "transparent";
modalCard.style.removeProperty("border-color");
modalCard.dataset.color = "custom";
modalCard.dataset.customColor = color;
}

View File

@ -1,4 +1,20 @@
document.addEventListener('DOMContentLoaded', () => {
// ===== Add Todo Button Handler (Android convention) =====
const addTodoBtn = document.querySelector('.sidebar-add-todo');
if (addTodoBtn) {
addTodoBtn.addEventListener('click', (e) => {
e.preventDefault();
if (!window.ShaarItRules) {
console.warn('[shaarit] ShaarItRules not available, cannot generate todo URL');
return;
}
const basePath = addTodoBtn.getAttribute('data-base-path') || '';
const todoUrl = window.ShaarItRules.generateTodoUrl();
const redirectUrl = `${basePath}/admin/shaare?post=${encodeURIComponent(todoUrl)}&tags=todo&title=%E2%9C%85%20`;
window.location.href = redirectUrl;
});
}
// ===== Theme Toggle =====
const themeCheckbox = document.getElementById('theme-toggle-checkbox');
const themeIconLight = document.getElementById('theme-icon-light');
@ -88,14 +104,19 @@ document.addEventListener('DOMContentLoaded', () => {
const HIDDEN_TAGS_STORAGE_KEY = 'shaarli_hidden_tags';
// Default system tags that are hidden by default
// Harmonisé avec ShaarIt Android (PRESET_SYSTEM_TAGS).
const DEFAULT_HIDDEN_TAGS = [
'note',
'shaarli-pin',
'shaarli-note',
'todo',
'shaarli-todo',
'shaarli-pin',
'note-color-*',
'notebg-*',
'notefilter-*',
'font-*',
'readitlater',
'brain-dump',
'shaarli-archive'
];
@ -1706,12 +1727,14 @@ document.addEventListener('DOMContentLoaded', () => {
const syncNoteCheckbox = () => {
if (!noteCheckbox) return;
noteCheckbox.checked = tags.some((tag) => /^note$/i.test(tag));
// Reconnaît les conventions Android (`note`, `#note`) et legacy (`shaarli-note`).
noteCheckbox.checked = tags.some((tag) => /^(note|#note|shaarli-note)$/i.test(tag));
};
const syncTodoCheckbox = () => {
if (!todoCheckbox) return;
todoCheckbox.checked = tags.some((tag) => /^shaarli-todo$/i.test(tag));
// Reconnaît les conventions Android (`todo`, `#todo`) et legacy (`shaarli-todo`).
todoCheckbox.checked = tags.some((tag) => /^(todo|#todo|shaarli-todo)$/i.test(tag));
};
// Helper functions to manage note emoji in title
@ -1956,6 +1979,66 @@ document.addEventListener('DOMContentLoaded', () => {
form.addEventListener('submit', commitInputValue);
// === Todo migration: shaarli-todo → todo + Android URL ===
// Lors de la sauvegarde d'un todo, remplacer le tag legacy par la norme Android
// et générer une URL Android si vide ou legacy.
form.addEventListener('submit', () => {
try {
if (!window.ShaarItRules) return;
const urlField = form.querySelector('input[name="lf_url"]');
const url = urlField ? (urlField.value || '').trim() : '';
// Vérifier si c'est un todo (tag shaarli-todo ou todo)
const hasTodoTag = tags.some((t) => /^(todo|shaarli-todo)$/i.test(t));
if (!hasTodoTag) return;
// 1. Remplacer shaarli-todo par todo
const hadLegacyTag = tags.some((t) => /^shaarli-todo$/i.test(t));
if (hadLegacyTag) {
tags = tags.filter((t) => !/^shaarli-todo$/i.test(t));
if (!tags.some((t) => /^todo$/i.test(t))) {
tags.push('todo');
}
updateHiddenTags();
}
// 2. Générer URL Android si vide ou legacy
if (!url || /^https?:\/\/shaarli-todo/.test(url) || url === 'http://shaarli-todo') {
const newUrl = window.ShaarItRules.generateTodoUrl();
urlField.value = newUrl;
}
} catch (e) {
console.warn('[shaarit] todo migration failed:', e);
}
});
// === Content-type auto-tagging (harmonisation ShaarIt Android) ===
// À la soumission, détecte le type de contenu depuis l'URL et injecte
// les tags automatiques (video, podcast, radio, music, article, news,
// social, repository+dev, shopping, image, pdf...).
// Les tags déjà présents ne sont jamais dupliqués ; aucun tag n'est
// supprimé. Désactivé pour les notes/todos (URLs internes).
form.addEventListener('submit', () => {
try {
if (!window.ShaarItRules) return;
const urlField = form.querySelector('input[name="lf_url"]');
const url = urlField ? (urlField.value || '').trim() : '';
if (!url) return;
// Ne pas auto-tagger les notes/todos (URLs internes reconnues par ShaarItRules).
const fakeLink = { url: url, tags: tags };
if (window.ShaarItRules.isNote(fakeLink) || window.ShaarItRules.isTodo(fakeLink)) return;
const detection = window.ShaarItRules.detectContentType(url);
if (!detection || !detection.tags || detection.tags.length === 0) return;
tags = window.ShaarItRules.mergeAutoTags(tags, detection.tags);
updateHiddenTags();
} catch (e) {
console.warn('[shaarit] content-type auto-tagging failed:', e);
}
});
updateHiddenTags();
syncReadLaterCheckbox();
syncNoteCheckbox();

View File

@ -0,0 +1,377 @@
/**
* shaarit-rules.js
*
* Règles métier partagées avec l'application Android ShaarIt.
* Source de vérité unique pour :
* - la détection des notes / todos / épinglés ;
* - les tags techniques cachés par défaut ;
* - la détection de type de contenu (video, podcast, radio, music, article,
* news, social, repository/dev, shopping, image, pdf, document) ;
* - la génération d'URLs internes (notes / todos).
*
* Non destructif : toutes les conventions existantes du thème web continuent
* d'être reconnues en lecture. Les règles Android viennent s'y ajouter.
*
* Exposé sur `window.ShaarItRules`.
*/
(function (global) {
"use strict";
// ---------------------------------------------------------------------------
// Tags système (cachés par défaut côté UI)
// ---------------------------------------------------------------------------
var PRESET_SYSTEM_TAGS = [
{ name: "note", desc: "Notes - Identifiant interne", hidden: true },
{ name: "shaarli-note", desc: "Notes - Alias legacy", hidden: true },
{ name: "todo", desc: "Tâches - Identifiant interne", hidden: true },
{ name: "shaarli-todo", desc: "Tâches - Alias legacy", hidden: true },
{ name: "shaarli-pin", desc: "Épinglé - Favoris en haut", hidden: true },
{ name: "note-color-*", desc: "Couleurs des notes (wildcard)", hidden: true },
{ name: "notebg-*", desc: "Fonds des notes (wildcard)", hidden: true },
{ name: "notefilter-*", desc: "Filtres des notes (wildcard)", hidden: true },
{ name: "font-*", desc: "Couleur de police (wildcard)", hidden: true },
{ name: "readitlater", desc: "À lire plus tard", hidden: true },
{ name: "brain-dump", desc: "Capture rapide d'idées", hidden: true },
{ name: "shaarli-archive", desc: "Archivé", hidden: false },
{ name: "shaarli-archiver",desc: "Archivé - Alias legacy", hidden: false }
];
// ---------------------------------------------------------------------------
// Détection d'entités (note / todo / pin) - compatibilité Android + legacy
// ---------------------------------------------------------------------------
function toLower(x) { return String(x || "").toLowerCase(); }
function asTagArray(tags) {
if (Array.isArray(tags)) return tags.map(toLower);
if (typeof tags === "string") {
return tags.split(/[\s,|]+/).map(toLower).filter(Boolean);
}
return [];
}
/**
* Détection d'une note.
* Compatible avec les règles Android :
* - URL `note://` (mais pas `note://todo-`)
* - URL `http://shaare` / `/shaare`
* - URL `https://shaarit.app/note/...`
* - Tag `note`, `#note`, `shaarli-note`
*/
function isNote(link) {
if (!link) return false;
var url = String(link.url || "").trim();
var tags = asTagArray(link.tags);
if (url) {
var u = url.toLowerCase();
if (u.indexOf("note://") === 0 && u.indexOf("note://todo-") !== 0) return true;
if (u.indexOf("http://shaare") === 0) return true;
if (u.indexOf("/shaare") === 0) return true;
if (u.indexOf("https://shaarit.app/note/") === 0) return true;
if (u.indexOf("http://shaarit.app/note/") === 0) return true;
}
for (var i = 0; i < tags.length; i++) {
if (tags[i] === "note" || tags[i] === "#note" || tags[i] === "shaarli-note") return true;
}
return false;
}
/**
* Détection d'une tâche.
* Compatible avec les règles Android :
* - URL `note://todo-...`
* - URL `https://shaarit.app/todo/...`
* - URL `http://shaarli-todo` (legacy web)
* - Tag `todo`, `#todo`, `shaarli-todo`
*/
function isTodo(link) {
if (!link) return false;
var url = String(link.url || "").trim();
var tags = asTagArray(link.tags);
if (url) {
var u = url.toLowerCase();
if (u.indexOf("note://todo-") === 0) return true;
if (u.indexOf("https://shaarit.app/todo/") === 0) return true;
if (u.indexOf("http://shaarit.app/todo/") === 0) return true;
if (u.indexOf("http://shaarli-todo") === 0) return true;
if (u.indexOf("https://shaarli-todo") === 0) return true;
}
for (var i = 0; i < tags.length; i++) {
if (tags[i] === "todo" || tags[i] === "#todo" || tags[i] === "shaarli-todo") return true;
}
return false;
}
/** Détection épinglé via tag `shaarli-pin`. */
function isPinned(link) {
if (!link) return false;
var tags = asTagArray(link.tags);
return tags.indexOf("shaarli-pin") !== -1;
}
/** Détection archive (tag `shaarli-archive` ou legacy `shaarli-archiver`). */
function isArchived(link) {
if (!link) return false;
var tags = asTagArray(link.tags);
return tags.indexOf("shaarli-archive") !== -1 || tags.indexOf("shaarli-archiver") !== -1;
}
// ---------------------------------------------------------------------------
// Génération d'URLs internes (style Android)
// ---------------------------------------------------------------------------
function randomId() {
// Identifiant court type base36, 12 caractères.
if (global.crypto && global.crypto.getRandomValues) {
var arr = new Uint8Array(9);
global.crypto.getRandomValues(arr);
var out = "";
for (var i = 0; i < arr.length; i++) {
out += ("0" + arr[i].toString(36)).slice(-2);
}
return out.substring(0, 12);
}
return (Date.now().toString(36) + Math.random().toString(36).slice(2, 10)).substring(0, 12);
}
function generateNoteUrl() {
return "https://shaarit.app/note/" + randomId();
}
function generateTodoUrl() {
return "https://shaarit.app/todo/" + randomId();
}
// ---------------------------------------------------------------------------
// Détection de type de contenu (ContentType, règles Android)
// ---------------------------------------------------------------------------
var CONTENT_TYPES = {
UNKNOWN: "unknown",
ARTICLE: "article",
VIDEO: "video",
PODCAST: "podcast",
IMAGE: "image",
PDF: "pdf",
REPOSITORY: "repository",
DOCUMENT: "document",
SOCIAL: "social",
SHOPPING: "shopping",
NEWSLETTER: "newsletter",
MUSIC: "music",
RADIO: "radio",
NEWS: "news"
};
// Tags auto-ajoutés par type de contenu (conformes règles Android UI).
var CONTENT_TYPE_TAGS = {
video: ["video"],
podcast: ["podcast"],
radio: ["radio"],
music: ["music"],
article: ["article"],
news: ["news"],
social: ["social"],
repository: ["repository", "dev"],
shopping: ["shopping"],
newsletter: ["newsletter"],
image: ["image"],
pdf: ["pdf"],
document: [],
unknown: []
};
function parseHost(url) {
try {
var u = new URL(url);
return (u.hostname || "").toLowerCase();
} catch (e) {
// Fallback très simple si URL invalide
var m = String(url || "").match(/^[a-z][a-z0-9+.-]*:\/\/([^/?#]+)/i);
return m ? m[1].toLowerCase() : "";
}
}
function hostContainsAny(host, list) {
for (var i = 0; i < list.length; i++) {
if (host.indexOf(list[i]) !== -1) return true;
}
return false;
}
// --- Audio : RADIO > PODCAST > MUSIC ---
var RADIO_HOSTS = [
"playerservices.streamtheworld.com", "icecast", "shoutcast",
"fluxradios.com", "tunein.com", "radio.garden", "mytuner-radio.com",
"iheart.com", "onlineradiobox.com", "radio.net"
];
function isRadio(url, host) {
if (/\.(m3u|m3u8|pls)(\?|$)/i.test(url)) return true;
if (hostContainsAny(host, RADIO_HOSTS)) return true;
if (host.indexOf("stream.") === 0 || host.indexOf("live.") === 0) return true;
if (host.indexOf("ici.radio-canada.ca") !== -1 || host.indexOf("radio-canada.ca") !== -1) {
if (/\/balados(\/|$)/i.test(url)) return false;
if (/\/(direct|premiere|audio-fil)(\/|$)/i.test(url)) return true;
}
return false;
}
var PODCAST_HOSTS = [
"podcasts.apple.com", "overcast.fm", "pocketcasts.com", "castbox.fm",
"stitcher.com", "acast.com", "anchor.fm", "libsyn.com",
"simplecast.com", "buzzsprout.com"
];
function isPodcast(url, host) {
if (hostContainsAny(host, PODCAST_HOSTS)) return true;
if (/open\.spotify\.com\/(show|episode)\//i.test(url)) return true;
if (/\/balados(\/|$)|\/ohdio\/balados(\/|$)/i.test(url)) return true;
if (/\.(xml|rss)(\?|$)/i.test(url)) return true;
return false;
}
var MUSIC_HOSTS = [
"music.apple.com", "deezer.com", "tidal.com", "music.youtube.com",
"bandcamp.com", "soundcloud.com", "mixcloud.com", "beatport.com"
];
function isMusic(url, host) {
if (hostContainsAny(host, MUSIC_HOSTS)) return true;
if (/open\.spotify\.com\/(track|album|artist|playlist)\//i.test(url)) return true;
return false;
}
// --- Catégories web ---
var VIDEO_HOSTS = ["youtube.com", "youtu.be", "vimeo.com", "dailymotion.com", "twitch.tv", "netflix.com"];
function isVideo(host) { return hostContainsAny(host, VIDEO_HOSTS); }
var SOCIAL_HOSTS = [
"facebook.com", "instagram.com", "tiktok.com", "twitter.com", "x.com",
"linkedin.com", "reddit.com", "snapchat.com", "pinterest.com", "mastodon"
];
function isSocial(host) { return hostContainsAny(host, SOCIAL_HOSTS); }
var REPO_HOSTS = ["github.com", "gitlab.com", "bitbucket.org", "stackoverflow.com"];
function isRepository(host) { return hostContainsAny(host, REPO_HOSTS); }
var SHOPPING_HOSTS = ["amazon", "ebay", "etsy.com", "aliexpress.com", "shopify"];
function isShopping(host) { return hostContainsAny(host, SHOPPING_HOSTS); }
var DOCUMENT_HOSTS = [
"docs.google.com", "drive.google.com", "notion.so", "trello.com",
"jira", "confluence"
];
function isDocument(host) { return hostContainsAny(host, DOCUMENT_HOSTS); }
var NEWS_HOSTS = [
"news", "nytimes", "lemonde", "bbc", "cnn", "reuters",
"theguardian", "lefigaro"
];
function isNews(host) { return hostContainsAny(host, NEWS_HOSTS); }
var NEWSLETTER_HOSTS = ["substack", "revue", "mailchimp"];
function isNewsletter(host) { return hostContainsAny(host, NEWSLETTER_HOSTS); }
var IMAGE_HOSTS = ["imgur.com", "flickr.com"];
function isImageUrl(url, host) {
if (/\.(jpe?g|png|gif|webp|bmp|svg)(\?|$)/i.test(url)) return true;
return hostContainsAny(host, IMAGE_HOSTS);
}
function isPdfUrl(url) {
return /\.pdf(\?|$)/i.test(url);
}
/**
* Détecte le type de contenu d'une URL (règles Android).
* @returns {{type: string, tags: string[]}}
*/
function detectContentType(url) {
var raw = String(url || "").trim();
if (!raw) return { type: CONTENT_TYPES.UNKNOWN, tags: [] };
// URLs internes : pas de détection
var low = raw.toLowerCase();
if (low.indexOf("note://") === 0 ||
low.indexOf("https://shaarit.app/note/") === 0 ||
low.indexOf("https://shaarit.app/todo/") === 0 ||
low.indexOf("http://shaare") === 0 ||
low.indexOf("/shaare") === 0 ||
low.indexOf("http://shaarli-todo") === 0) {
return { type: CONTENT_TYPES.UNKNOWN, tags: [] };
}
var host = parseHost(raw);
// 1. Fichiers (priorité haute sur l'extension)
if (isPdfUrl(raw)) return { type: CONTENT_TYPES.PDF, tags: CONTENT_TYPE_TAGS.pdf };
if (isImageUrl(raw, host))return { type: CONTENT_TYPES.IMAGE, tags: CONTENT_TYPE_TAGS.image };
// 2. Audio : RADIO > PODCAST > MUSIC
if (isRadio(raw, host)) return { type: CONTENT_TYPES.RADIO, tags: CONTENT_TYPE_TAGS.radio };
if (isPodcast(raw, host)) return { type: CONTENT_TYPES.PODCAST, tags: CONTENT_TYPE_TAGS.podcast };
if (isMusic(raw, host)) return { type: CONTENT_TYPES.MUSIC, tags: CONTENT_TYPE_TAGS.music };
// 3. Vidéo
if (isVideo(host)) return { type: CONTENT_TYPES.VIDEO, tags: CONTENT_TYPE_TAGS.video };
// 4. Plateformes spécifiques
if (isRepository(host)) return { type: CONTENT_TYPES.REPOSITORY, tags: CONTENT_TYPE_TAGS.repository };
if (isDocument(host)) return { type: CONTENT_TYPES.DOCUMENT, tags: CONTENT_TYPE_TAGS.document };
if (isSocial(host)) return { type: CONTENT_TYPES.SOCIAL, tags: CONTENT_TYPE_TAGS.social };
if (isShopping(host)) return { type: CONTENT_TYPES.SHOPPING, tags: CONTENT_TYPE_TAGS.shopping };
if (isNewsletter(host)) return { type: CONTENT_TYPES.NEWSLETTER, tags: CONTENT_TYPE_TAGS.newsletter };
if (isNews(host)) return { type: CONTENT_TYPES.NEWS, tags: CONTENT_TYPE_TAGS.news };
return { type: CONTENT_TYPES.UNKNOWN, tags: [] };
}
/**
* Fusion d'un tableau de tags existants avec les tags auto-détectés.
* Conserve l'ordre existant, ajoute uniquement les tags manquants.
*/
function mergeAutoTags(existingTags, autoTags) {
var list = Array.isArray(existingTags) ? existingTags.slice() : [];
var lower = list.map(toLower);
(autoTags || []).forEach(function (t) {
var clean = String(t || "").trim();
if (!clean) return;
if (lower.indexOf(clean.toLowerCase()) === -1) {
list.push(clean);
lower.push(clean.toLowerCase());
}
});
return list;
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
var api = {
PRESET_SYSTEM_TAGS: PRESET_SYSTEM_TAGS,
CONTENT_TYPES: CONTENT_TYPES,
CONTENT_TYPE_TAGS: CONTENT_TYPE_TAGS,
isNote: isNote,
isTodo: isTodo,
isPinned: isPinned,
isArchived: isArchived,
generateNoteUrl: generateNoteUrl,
generateTodoUrl: generateTodoUrl,
detectContentType: detectContentType,
mergeAutoTags: mergeAutoTags
};
global.ShaarItRules = api;
})(typeof window !== "undefined" ? window : this);

View File

@ -47,11 +47,16 @@
<!-- {* ----- no links ----- *} -->
{if="count($links)==0"}
{if="in_array('note', $active_search_tags) || in_array('shaarli-note', $active_search_tags) || in_array('todo', $active_search_tags) || in_array('shaarli-todo', $active_search_tags) || in_array('shaarli-archive', $active_search_tags) || in_array('shaarli-archiver', $active_search_tags)"}
<div class="links-list view-grid" id="links-list"></div>
{include="linklist.paging"}
{else}
<div class="empty-state" role="status" aria-live="polite">
<div class="empty-state-icon" aria-hidden="true"><i class="mdi mdi-bookmark-off-outline"></i></div>
<h2 class="empty-state-title">Aucun bookmark trouvé</h2>
<p class="empty-state-text">{if="!empty($search_term)"}Aucun résultat pour : <strong>{$search_term}</strong>{else}Commencez à ajouter des bookmarks pour les voir apparaître ici.{/if}</p>
</div>
{/if}
{else}
<!-- {* ----- at least one link ----- *} -->
<div class="links-list view-grid" id="links-list">

View File

@ -0,0 +1,104 @@
<?php
/**
* migrate-todos.php
*
* Script de migration batch : remplace tous les todos legacy (shaarli-todo)
* par la norme Android (tag `todo` + URL `https://shaarit.app/todo/{uuid}`).
*
* À exécuter une seule fois via CLI ou interface web (si accessible).
* Usage: php migrate-todos.php
*/
// Déterminer le chemin racine de Shaarli
$rootDir = dirname(dirname(dirname(__FILE__)));
require_once $rootDir . '/index.php';
// Vérifier que Shaarli est chargé
if (!isset($GLOBALS['shaarli'])) {
die("Erreur : Shaarli non chargé.\n");
}
$linkDb = $GLOBALS['shaarli']->getContainer()->get('db');
if (!$linkDb) {
die("Erreur : base de données non accessible.\n");
}
// Récupérer tous les liens avec le tag shaarli-todo
$allLinks = $linkDb->getLinks();
$todosToMigrate = [];
foreach ($allLinks as $link) {
$tags = explode(' ', $link->getTags());
$hasTodoTag = in_array('shaarli-todo', $tags, true);
if ($hasTodoTag) {
$todosToMigrate[] = $link;
}
}
if (empty($todosToMigrate)) {
echo "Aucun todo à migrer.\n";
exit(0);
}
echo "Trouvé " . count($todosToMigrate) . " todo(s) à migrer.\n";
// Fonction pour générer un UUID court (style Android)
function generateTodoUuid() {
if (function_exists('random_bytes')) {
$bytes = random_bytes(9);
$out = '';
for ($i = 0; $i < 9; $i++) {
$out .= str_pad(base_convert(ord($bytes[$i]), 10, 36), 2, '0', STR_PAD_LEFT);
}
return substr($out, 0, 12);
}
return substr(base_convert(time(), 10, 36) . base_convert(mt_rand(), 10, 36), 0, 12);
}
// Migrer chaque todo
$migrated = 0;
$errors = 0;
foreach ($todosToMigrate as $link) {
try {
$tags = explode(' ', $link->getTags());
// 1. Remplacer shaarli-todo par todo
$tags = array_filter($tags, function ($t) {
return $t !== 'shaarli-todo';
});
$tags = array_values($tags);
if (!in_array('todo', $tags, true)) {
$tags[] = 'todo';
}
$link->setTags($tags);
// 2. Générer URL Android si vide ou legacy
$url = $link->getUrl();
if (empty($url) || strpos($url, 'http://shaarli-todo') === 0 || strpos($url, 'https://shaarli-todo') === 0) {
$uuid = generateTodoUuid();
$link->setUrl('https://shaarit.app/todo/' . $uuid);
}
// Sauvegarder
$linkDb->save($link);
$migrated++;
echo "✓ Migré : {$link->getTitle()} ({$link->getId()})\n";
} catch (Exception $e) {
$errors++;
echo "✗ Erreur : {$link->getTitle()} - {$e->getMessage()}\n";
}
}
echo "\n=== Résumé ===\n";
echo "Migrés : $migrated\n";
echo "Erreurs : $errors\n";
echo "Total : " . count($todosToMigrate) . "\n";
if ($errors === 0) {
echo "\n✓ Migration terminée avec succès !\n";
exit(0);
} else {
echo "\n⚠ Migration terminée avec $errors erreur(s).\n";
exit(1);
}

View File

@ -49,7 +49,7 @@ Bookmarklet detection logic
<i class="mdi mdi-calendar-today" aria-hidden="true"></i>
<span>Quotidien</span>
</a>
<a href="{$base_path}/?searchtags=shaarli-todo" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Mes tâches">
<a href="{$base_path}/?searchtags=todo" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Mes tâches">
<i class="mdi mdi-check-circle-outline" aria-hidden="true"></i>
<span>Mes tâches</span>
</a>
@ -93,7 +93,7 @@ Bookmarklet detection logic
<a href="{$base_path}/admin/shaare?tags=note" class="sidebar-add-segment" title="Note">
<i class="mdi mdi-note-text-outline"></i>
</a>
<a href="{$base_path}/admin/shaare?post=http%3A%2F%2Fshaarli-todo&tags=shaarli-todo&title=%E2%9C%85%20" class="sidebar-add-segment" title="Todo">
<a href="#" class="sidebar-add-segment sidebar-add-todo" title="Todo" data-base-path="{$base_path}">
<i class="mdi mdi-check-circle-outline"></i>
</a>
</div>
@ -154,7 +154,7 @@ Bookmarklet detection logic
<i class="mdi mdi-calendar" aria-hidden="true"></i>
<span>QUOTIDIEN</span>
</a>
<a href="{$base_path}/?searchtags=shaarli-todo" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Mes tâches">
<a href="{$base_path}/?searchtags=todo" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Mes tâches">
<i class="mdi mdi-check-circle-outline" aria-hidden="true"></i>
<span>TÂCHES</span>
</a>

View File

@ -430,13 +430,19 @@
(function() {
const STORAGE_KEY = 'shaarli_hidden_tags';
// Harmonisé avec ShaarIt Android (PRESET_SYSTEM_TAGS).
const PRESET_TAGS = [
{ name: 'note', desc: 'Notes - Internal note identifier' },
{ name: 'shaarli-note', desc: 'Notes - Legacy alias' },
{ name: 'todo', desc: 'Tasks - Internal todo identifier' },
{ name: 'shaarli-todo', desc: 'Tasks - Legacy alias' },
{ name: 'shaarli-pin', desc: 'Pinned - Keeps bookmarks at top' },
{ name: 'note-color-*', desc: 'Note Colors - Wildcard for color tags' },
{ name: 'notebg-*', desc: 'Note Backgrounds - Wildcard for background tags' },
{ name: 'notefilter-*', desc: 'Note Filters - Wildcard for filter tags' },
{ name: 'font-*', desc: 'Note Font Colors - Wildcard' },
{ name: 'readitlater', desc: 'Read Later - Temporary reading list' },
{ name: 'brain-dump', desc: 'Brain dump - Quick idea capture' },
{ name: 'shaarli-archive', desc: 'Archived - Archived notes' }
];