CI / build (push) Failing after 6m2s
Introduce a Node build system for minified CSS/JS with content hashes, add PWA support, i18n helpers, accessibility improvements, and more translatable strings. Move theme configuration from bookmarks to localStorage, add Playwright tests, and set up CI workflows.
943 lines
44 KiB
HTML
943 lines
44 KiB
HTML
<!DOCTYPE html>
|
|
<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
|
|
|
|
<head>
|
|
{$pageName="tools"}
|
|
{include="includes"}
|
|
</head>
|
|
|
|
<body>
|
|
{include="page.header"}
|
|
<div id="toolsdiv" class="page-tools container">
|
|
<div class="row">
|
|
<div class="col-md-8 col-md-offset-2">
|
|
<div class="card">
|
|
<div class="card-header">{'Settings'|t}</div>
|
|
<div class="list-group">
|
|
<a class="list-group-item list-group-item-action ripple" href="#" id="themes-panel-toggle">
|
|
<div class="list-sortable-handle">
|
|
<i class="mdi mdi-palette"></i>
|
|
</div>
|
|
<div class="list-group-item-content">
|
|
<div class="list-group-item-label">{'Themes'|t}</div>
|
|
<div class="list-group-item-sublabel">{'Choose application visual theme'|t}</div>
|
|
</div>
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
</a>
|
|
<a class="list-group-item list-group-item-action ripple" href="{$base_path}/admin/configure">
|
|
<div class="list-sortable-handle">
|
|
<i class="mdi mdi-cog"></i>
|
|
</div>
|
|
<div class="list-group-item-content">
|
|
<div class="list-group-item-label">{'Configure your Shaarli'|t}</div>
|
|
<div class="list-group-item-sublabel">{'Change Title, timezone...'|t}</div>
|
|
</div>
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
</a>
|
|
{if="!$openshaarli"}
|
|
<a class="list-group-item list-group-item-action ripple" href="{$base_path}/admin/password">
|
|
<div class="list-sortable-handle">
|
|
<i class="mdi mdi-lock"></i>
|
|
</div>
|
|
<div class="list-group-item-content">
|
|
<div class="list-group-item-label">{'Change password'|t}</div>
|
|
<div class="list-group-item-sublabel">{'Change your password'|t}</div>
|
|
</div>
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
</a>
|
|
{/if}
|
|
<a class="list-group-item list-group-item-action ripple" href="{$base_path}/admin/plugins">
|
|
<div class="list-sortable-handle">
|
|
<i class="mdi mdi-puzzle"></i>
|
|
</div>
|
|
<div class="list-group-item-content">
|
|
<div class="list-group-item-label">{'Plugin administration'|t}</div>
|
|
<div class="list-group-item-sublabel">{'Enable, disable and configure plugins'|t}</div>
|
|
</div>
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
</a>
|
|
<a class="list-group-item list-group-item-action ripple" href="{$base_path}/admin/server">
|
|
<div class="list-sortable-handle">
|
|
<i class="mdi mdi-server"></i>
|
|
</div>
|
|
<div class="list-group-item-content">
|
|
<div class="list-group-item-label">{'Server administration'|t}</div>
|
|
<div class="list-group-item-sublabel">{'Check instance\'s server configuration'|t}</div>
|
|
</div>
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
</a>
|
|
<a class="list-group-item list-group-item-action ripple" href="{$base_path}/admin/tags">
|
|
<div class="list-sortable-handle">
|
|
<i class="mdi mdi-tag-multiple"></i>
|
|
</div>
|
|
<div class="list-group-item-content">
|
|
<div class="list-group-item-label">{'Manage Tags'|t}</div>
|
|
<div class="list-group-item-sublabel">{'Rename or delete a tag in all links'|t}</div>
|
|
</div>
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
</a>
|
|
<a class="list-group-item list-group-item-action ripple" href="#" id="hidden-tags-toggle">
|
|
<div class="list-sortable-handle">
|
|
<i class="mdi mdi-tag-off-outline"></i>
|
|
</div>
|
|
<div class="list-group-item-content">
|
|
<div class="list-group-item-label">{'Hidden Tags'|t}</div>
|
|
<div class="list-group-item-sublabel">{'Manage system tags hidden from display'|t}</div>
|
|
</div>
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
</a>
|
|
<a class="list-group-item list-group-item-action ripple" href="{$base_path}/admin/import">
|
|
<div class="list-sortable-handle">
|
|
<i class="mdi mdi-file-import"></i>
|
|
</div>
|
|
<div class="list-group-item-content">
|
|
<div class="list-group-item-label">{'Import'|t}</div>
|
|
<div class="list-group-item-sublabel">{'Import Netscape html bookmarks'|t}</div>
|
|
</div>
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
</a>
|
|
<a class="list-group-item list-group-item-action ripple" href="{$base_path}/admin/export">
|
|
<div class="list-sortable-handle">
|
|
<i class="mdi mdi-file-export"></i>
|
|
</div>
|
|
<div class="list-group-item-content">
|
|
<div class="list-group-item-label">{'Export'|t}</div>
|
|
<div class="list-group-item-sublabel">{'Export Netscape html bookmarks'|t}</div>
|
|
</div>
|
|
<i class="mdi mdi-chevron-right"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Themes Management Panel -->
|
|
<div class="row" id="themes-panel" style="display: none;">
|
|
<div class="col-md-8 col-md-offset-2">
|
|
<div class="card themes-card">
|
|
<div class="card-header">
|
|
<span><i class="mdi mdi-palette"></i> {'Application Themes'|t}</span>
|
|
<button type="button" class="btn btn-sm btn-secondary" id="close-themes-panel">
|
|
<i class="mdi mdi-close"></i>
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="themes-info">
|
|
<i class="mdi mdi-information-outline"></i>
|
|
<span>{'Choose your preferred visual theme. The theme configuration is saved locally in your browser.'|t}</span>
|
|
</div>
|
|
|
|
<div class="themes-grid" id="themes-grid">
|
|
<!-- Filled by JavaScript -->
|
|
</div>
|
|
|
|
<div class="themes-actions">
|
|
<span id="theme-save-status" class="save-status"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden Tags Management Panel -->
|
|
<div class="row" id="hidden-tags-panel" style="display: none;">
|
|
<div class="col-md-8 col-md-offset-2">
|
|
<div class="card hidden-tags-card">
|
|
<div class="card-header">
|
|
<span><i class="mdi mdi-tag-off-outline"></i> {'Hidden Tags'|t}</span>
|
|
<button type="button" class="btn btn-sm btn-secondary" id="close-hidden-tags">
|
|
<i class="mdi mdi-close"></i>
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Info Banner -->
|
|
<div class="hidden-tags-info">
|
|
<i class="mdi mdi-information-outline"></i>
|
|
<span>{'Hidden tags are functional tags used by the application that remain invisible to users. They are still fully operational for internal features.'|t}</span>
|
|
</div>
|
|
|
|
<!-- Preset System Tags -->
|
|
<div class="hidden-tags-section">
|
|
<h4><i class="mdi mdi-tag-check-outline"></i> {'Preset System Tags'|t}</h4>
|
|
<p class="text-muted">{'Toggle visibility for built-in system tags'|t}</p>
|
|
<div id="preset-tags-grid" class="preset-tags-grid">
|
|
<!-- Filled by JavaScript -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Hidden Tags -->
|
|
<div class="hidden-tags-section">
|
|
<h4><i class="mdi mdi-tag-plus-outline"></i> {'Custom Hidden Tags'|t}</h4>
|
|
<p class="text-muted">{'Add your own tags to hide from display (supports wildcards like note-*)'|t}</p>
|
|
<div class="custom-tags-input-group">
|
|
<input type="text" id="custom-tag-input" class="form-control" placeholder="{'Enter tag name...'|t}">
|
|
<button type="button" class="btn btn-primary" id="add-custom-tag">
|
|
<i class="mdi mdi-plus"></i> {'Add'|t}
|
|
</button>
|
|
</div>
|
|
<div id="custom-tags-list" class="custom-tags-list">
|
|
<!-- Filled by JavaScript -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="hidden-tags-actions">
|
|
<button type="button" class="btn btn-secondary" id="reset-hidden-tags">
|
|
<i class="mdi mdi-refresh"></i> {'Reset to Defaults'|t}
|
|
</button>
|
|
<span id="save-status" class="save-status"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{if="!empty($linkcount)"}
|
|
<div class="row">
|
|
<div class="col-md-8 col-md-offset-2">
|
|
<div class="card">
|
|
<div class="card-header">{'Statistics'|t}</div>
|
|
<div class="card-body">
|
|
<div class="key-value-list">
|
|
<div class="key-value-item">
|
|
<div class="key-value-label">{'Total Links'|t}</div>
|
|
<div class="key-value-data">{$linkcount}</div>
|
|
</div>
|
|
<div class="key-value-item">
|
|
<div class="key-value-label">{'Private Links'|t}</div>
|
|
<div class="key-value-data">{$privateLinkcount}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{if="isset($tools_plugin)"}
|
|
<div class="row">
|
|
<div class="col-md-8 col-md-offset-2">
|
|
<div class="card">
|
|
<div class="card-header">{'Plugin settings'|t}</div>
|
|
<div class="card-body">
|
|
{loop="$tools_plugin"}
|
|
{$value}
|
|
{/loop}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="row">
|
|
<div class="col-md-8 col-md-offset-2">
|
|
<div class="card">
|
|
<div class="card-header">{'Bookmarklet'|t}</div>
|
|
<div class="card-body">
|
|
<p class="text-muted" style="margin-bottom: 1.5rem;">
|
|
{'Drag these buttons to your bookmarks bar to quickly add links from any page.'|t}
|
|
</p>
|
|
<div class="bookmarklet-actions">
|
|
<a class="btn btn-primary" href="javascript:(function(){var%20url%20=%20location.href;var%20title%20=%20document.title%20||%20url;var%20desc=document.getSelection().toString();if(desc.length>4000){desc=desc.substr(0,4000)+'...';alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}');}window.open('{$pageabsaddr}admin/shaare?post='%20+%20encodeURIComponent(url)+'&title='%20+%20encodeURIComponent(title)+'&description='%20+%20encodeURIComponent(desc)+'&source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1');})();">
|
|
<i class="mdi mdi-bookmark-plus"></i> {'Shaare link'|t}
|
|
</a>
|
|
<a class="btn btn-secondary" href="javascript:(function(){var desc=document.getSelection().toString();if(desc.length>4000){desc=desc.substr(0,4000)+'...';alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}');}window.open('{$pageabsaddr}?private=1&post='+'&description='%20+%20encodeURIComponent(desc)+'&source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1');})();">
|
|
<i class="mdi mdi-note-plus"></i> {'Add Note'|t}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-8 col-md-offset-2">
|
|
<div class="card">
|
|
<div class="card-header">{'3rd party'|t}</div>
|
|
<div class="card-body">
|
|
<div class="third-party-links">
|
|
<a href="https://addons.mozilla.org/fr/firefox/addon/shaarli/" target="_blank" class="btn btn-secondary">
|
|
<i class="mdi mdi-firefox"></i> Firefox
|
|
</a>
|
|
<a href="https://chromewebstore.google.com/detail/add-to-shaarli/jhfblapoehcfajokolimghdfmeeakbee" target="_blank" class="btn btn-secondary">
|
|
<i class="mdi mdi-google-chrome"></i> Chrome
|
|
</a>
|
|
<a href="https://f-droid.org/fr/packages/com.dimtion.shaarlier/" target="_blank" class="btn btn-secondary">
|
|
<i class="mdi mdi-android"></i> Shaarlier
|
|
</a>
|
|
<a href="https://stakali.toneiv.eu/" target="_blank" class="btn btn-secondary">
|
|
<i class="mdi mdi-android"></i> Stakali
|
|
</a>
|
|
<a href="https://github.com/lockcp/ShaarliOS" target="_blank" class="btn btn-secondary">
|
|
<i class="mdi mdi-apple-ios"></i> iOS
|
|
</a>
|
|
</div>
|
|
<p class="text-center text-muted" style="margin-top: 1.5rem;">
|
|
Other <a href="https://shaarli.readthedocs.io/en/master/Community-and-related-software.html" target="_blank">{'Community and related software'|t}</a>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
{include="page.footer"}
|
|
|
|
<style>
|
|
/* Themes Panel Styles */
|
|
.themes-card .card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.themes-info {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
padding: 1rem;
|
|
background: linear-gradient(135deg, rgba(0, 212, 170, 0.1) 0%, rgba(14, 165, 233, 0.1) 100%);
|
|
border: 1px solid rgba(0, 212, 170, 0.2);
|
|
border-radius: 0.75rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.themes-info i {
|
|
font-size: 1.25rem;
|
|
color: var(--primary);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.themes-info span {
|
|
font-size: 0.9rem;
|
|
color: var(--text-muted);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.themes-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.theme-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 1rem;
|
|
background: var(--bg-card);
|
|
border: 2px solid var(--border-light);
|
|
border-radius: 0.75rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.theme-item:hover {
|
|
border-color: rgba(0, 212, 170, 0.4);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.theme-item.active {
|
|
border-color: var(--primary);
|
|
background: rgba(var(--primary-rgb), 0.05);
|
|
}
|
|
|
|
.theme-preview {
|
|
width: 100%;
|
|
height: 80px;
|
|
border-radius: 0.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
border: 1px solid var(--border-light);
|
|
}
|
|
|
|
.theme-preview-top {
|
|
height: 30%;
|
|
background: var(--preview-primary);
|
|
}
|
|
|
|
.theme-preview-bottom {
|
|
height: 70%;
|
|
background: var(--preview-bg);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.theme-preview-card {
|
|
width: 60%;
|
|
height: 50%;
|
|
background: var(--preview-surface);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.theme-name {
|
|
font-weight: 500;
|
|
font-size: 1rem;
|
|
color: var(--text-primary);
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.theme-desc {
|
|
font-size: 0.8rem;
|
|
color: var(--text-muted);
|
|
text-align: center;
|
|
}
|
|
|
|
[data-theme="light"] .themes-info {
|
|
background: linear-gradient(135deg, rgba(0, 107, 90, 0.05) 0%, rgba(0, 119, 182, 0.05) 100%);
|
|
}
|
|
|
|
/* Hidden Tags Panel Styles */
|
|
.hidden-tags-card .card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.hidden-tags-info {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
padding: 1rem;
|
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(99, 102, 241, 0.1) 100%);
|
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
border-radius: 0.75rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.hidden-tags-info i {
|
|
font-size: 1.25rem;
|
|
color: #5b8def;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.hidden-tags-info span {
|
|
font-size: 0.9rem;
|
|
color: var(--text-muted);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.hidden-tags-section {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.hidden-tags-section h4 {
|
|
font-size: 1.1rem;
|
|
margin-bottom: 0.5rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.hidden-tags-section h4 i {
|
|
color: #5b8def;
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.preset-tags-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 0.75rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.preset-tag-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 1rem;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-light);
|
|
border-radius: 0.5rem;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.preset-tag-item:hover {
|
|
border-color: rgba(99, 102, 241, 0.3);
|
|
background: rgba(99, 102, 241, 0.05);
|
|
}
|
|
|
|
.preset-tag-checkbox {
|
|
width: 20px;
|
|
height: 20px;
|
|
accent-color: #5b8def;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.preset-tag-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.preset-tag-name {
|
|
font-weight: 500;
|
|
font-size: 0.95rem;
|
|
color: var(--text-primary);
|
|
font-family: 'Fira Code', monospace;
|
|
background: rgba(99, 102, 241, 0.1);
|
|
padding: 0.15rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
.preset-tag-desc {
|
|
font-size: 0.8rem;
|
|
color: var(--text-muted);
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.custom-tags-input-group {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.custom-tags-input-group input {
|
|
flex: 1;
|
|
}
|
|
|
|
.custom-tags-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin-top: 1rem;
|
|
min-height: 40px;
|
|
}
|
|
|
|
.custom-tag-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.4rem 0.75rem;
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(91, 141, 239, 0.15) 100%);
|
|
border: 1px solid rgba(99, 102, 241, 0.3);
|
|
border-radius: 2rem;
|
|
font-size: 0.9rem;
|
|
font-family: 'Fira Code', monospace;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.custom-tag-chip .remove-tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: rgba(239, 68, 68, 0.2);
|
|
color: #ef4444;
|
|
font-size: 0.75rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.custom-tag-chip .remove-tag:hover {
|
|
background: #ef4444;
|
|
color: white;
|
|
}
|
|
|
|
.no-custom-tags {
|
|
color: var(--text-muted);
|
|
font-style: italic;
|
|
font-size: 0.9rem;
|
|
padding: 0.5rem 0;
|
|
}
|
|
|
|
.hidden-tags-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-top: 1.5rem;
|
|
padding-top: 1.5rem;
|
|
border-top: 1px solid var(--border-light);
|
|
}
|
|
|
|
.save-status {
|
|
font-size: 0.9rem;
|
|
color: #10b981;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.save-status.show {
|
|
opacity: 1;
|
|
}
|
|
|
|
[data-theme="light"] .hidden-tags-info {
|
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(99, 102, 241, 0.08) 100%);
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// Themes Management
|
|
(function() {
|
|
const THEMES = [
|
|
{ id: 'DEFAULT', name: 'ShaarIt', desc: 'Original theme', color: '#00D4AA', bg: '#0A1628', surface: '#0D1B2A', hasLight: true },
|
|
{ id: 'GITHUB', name: 'GitHub', desc: 'Soft contrast', color: '#58A6FF', bg: '#0D1117', surface: '#161B22', hasLight: true },
|
|
{ id: 'LINEAR', name: 'Linear', desc: 'Subtle gradients', color: '#5E6AD2', bg: '#12131A', surface: '#1B1C24', hasLight: false },
|
|
{ id: 'SPOTIFY', name: 'Spotify', desc: 'Pure black & green', color: '#1DB954', bg: '#000000', surface: '#121212', hasLight: false },
|
|
{ id: 'NOTION', name: 'Notion', desc: 'Clean reading', color: '#529CCA', bg: '#191919', surface: '#202020', hasLight: false },
|
|
{ id: 'DISCORD', name: 'Discord', desc: 'Community standard', color: '#5865F2', bg: '#313338', surface: '#2B2D31', hasLight: false },
|
|
{ id: 'DRACULA', name: 'Dracula', desc: 'Pink & purple accents', color: '#BD93F9', bg: '#282A36', surface: '#21222C', hasLight: false },
|
|
{ id: 'ONE_DARK_PRO', name: 'One Dark', desc: 'Atom inspired', color: '#61AFEF', bg: '#282C34', surface: '#21252B', hasLight: false },
|
|
{ id: 'TOKYO_NIGHT', name: 'Tokyo Night', desc: 'Neon blue', color: '#7AA2F7', bg: '#1A1B26', surface: '#16171F', hasLight: false },
|
|
{ id: 'NORD', name: 'Nord', desc: 'Arctic style', color: '#88C0D0', bg: '#2E3440', surface: '#3B4252', hasLight: false },
|
|
{ id: 'NIGHT_OWL', name: 'Night Owl', desc: 'Optimized for night', color: '#7FDBCA', bg: '#011627', surface: '#0B2942', hasLight: false },
|
|
{ id: 'ANTHRACITE', name: 'Anthracite', desc: 'Material dark', color: '#BB86FC', bg: '#121212', surface: '#1E1E1E', hasLight: false },
|
|
{ id: 'CYBERPUNK', name: 'Cyberpunk', desc: 'Vibrant neon', color: '#00FFFF', bg: '#0A0A14', surface: '#12121E', hasLight: false },
|
|
{ id: 'NAVY_ELEGANCE', name: 'Navy Élégance', desc: 'Deep blue & gold', color: '#D4AF37', bg: '#0B1929', surface: '#0F2035', hasLight: false },
|
|
{ id: 'EARTHY', name: 'Tons Terreux', desc: 'Organic feel', color: '#81C784', bg: '#1A1510', surface: '#231E18', hasLight: false }
|
|
];
|
|
|
|
function getActiveTheme() {
|
|
return localStorage.getItem('shaarit_theme_id') || 'DEFAULT';
|
|
}
|
|
|
|
function showSaveStatus() {
|
|
const status = document.getElementById('theme-save-status');
|
|
if (status) {
|
|
status.textContent = 'Saved! Refreshing...';
|
|
status.classList.add('show');
|
|
}
|
|
}
|
|
|
|
function renderThemes() {
|
|
const container = document.getElementById('themes-grid');
|
|
if (!container) return;
|
|
|
|
const activeTheme = getActiveTheme();
|
|
container.innerHTML = '';
|
|
|
|
THEMES.forEach(theme => {
|
|
const item = document.createElement('div');
|
|
item.className = 'theme-item' + (activeTheme === theme.id ? ' active' : '');
|
|
item.dataset.themeId = theme.id;
|
|
item.dataset.hasLight = String(theme.hasLight);
|
|
|
|
const preview = document.createElement('div');
|
|
preview.className = 'theme-preview';
|
|
preview.style.setProperty('--preview-primary', theme.color);
|
|
preview.style.setProperty('--preview-bg', theme.bg);
|
|
preview.style.setProperty('--preview-surface', theme.surface);
|
|
preview.innerHTML = '<div class="theme-preview-top"></div><div class="theme-preview-bottom"><div class="theme-preview-card"></div></div>';
|
|
|
|
const name = document.createElement('div');
|
|
name.className = 'theme-name';
|
|
name.textContent = theme.name;
|
|
|
|
const desc = document.createElement('div');
|
|
desc.className = 'theme-desc';
|
|
desc.textContent = theme.desc;
|
|
|
|
item.appendChild(preview);
|
|
item.appendChild(name);
|
|
item.appendChild(desc);
|
|
container.appendChild(item);
|
|
});
|
|
|
|
container.querySelectorAll('.theme-item').forEach(item => {
|
|
item.addEventListener('click', function() {
|
|
const themeId = this.dataset.themeId;
|
|
const hasLight = this.dataset.hasLight === 'true';
|
|
|
|
// Update UI immediately
|
|
container.querySelectorAll('.theme-item').forEach(el => el.classList.remove('active'));
|
|
this.classList.add('active');
|
|
|
|
// Apply theme locally
|
|
localStorage.setItem('shaarit_theme_id', themeId);
|
|
document.documentElement.setAttribute('data-theme-id', themeId);
|
|
|
|
// Handle light mode restriction
|
|
if (!hasLight) {
|
|
localStorage.setItem('theme', 'dark');
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
// Trigger event for sidebar to update its switch
|
|
window.dispatchEvent(new Event('themeChanged'));
|
|
}
|
|
|
|
showSaveStatus();
|
|
|
|
// Sync to bookmark in background
|
|
syncThemeToBookmark(themeId).then(() => {
|
|
// Refresh to ensure everything applies cleanly (plugins etc)
|
|
setTimeout(() => window.location.reload(), 500);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function syncThemeToBookmark(themeId) {
|
|
try {
|
|
if (!window.ShaaritThemeConfig) {
|
|
console.warn('ShaaritThemeConfig helper not available');
|
|
return;
|
|
}
|
|
const currentMode = localStorage.getItem('theme')
|
|
|| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
await window.ShaaritThemeConfig.save({
|
|
themes: THEMES,
|
|
default: themeId,
|
|
mode: currentMode,
|
|
});
|
|
} catch (e) {
|
|
console.error('Failed to sync theme to bookmark', e);
|
|
}
|
|
}
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const toggleBtn = document.getElementById('themes-panel-toggle');
|
|
const panel = document.getElementById('themes-panel');
|
|
const closeBtn = document.getElementById('close-themes-panel');
|
|
|
|
if (toggleBtn && panel) {
|
|
toggleBtn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
panel.style.display = panel.style.display === 'none' ? '' : 'none';
|
|
if (panel.style.display !== 'none') {
|
|
renderThemes();
|
|
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
});
|
|
}
|
|
|
|
if (closeBtn && panel) {
|
|
closeBtn.addEventListener('click', function() {
|
|
panel.style.display = 'none';
|
|
});
|
|
}
|
|
});
|
|
})();
|
|
|
|
// Hidden Tags Management
|
|
(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' }
|
|
];
|
|
|
|
const DEFAULT_HIDDEN_TAGS = PRESET_TAGS.map(t => t.name);
|
|
|
|
function getHiddenTags() {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (stored) return JSON.parse(stored);
|
|
} catch (e) {
|
|
console.error('Error loading hidden tags:', e);
|
|
}
|
|
return [...DEFAULT_HIDDEN_TAGS];
|
|
}
|
|
|
|
function saveHiddenTags(tags) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(tags));
|
|
showSaveStatus();
|
|
} catch (e) {
|
|
console.error('Error saving hidden tags:', e);
|
|
}
|
|
}
|
|
|
|
function showSaveStatus() {
|
|
const status = document.getElementById('save-status');
|
|
if (status) {
|
|
status.textContent = '{'Saved!'|t}';
|
|
status.classList.add('show');
|
|
setTimeout(() => status.classList.remove('show'), 2000);
|
|
}
|
|
}
|
|
|
|
function isWildcardTag(tag) {
|
|
return tag.includes('*');
|
|
}
|
|
|
|
function matchesWildcard(pattern, tag) {
|
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
|
|
return regex.test(tag);
|
|
}
|
|
|
|
function renderPresetTags() {
|
|
const container = document.getElementById('preset-tags-grid');
|
|
if (!container) return;
|
|
|
|
const hiddenTags = getHiddenTags();
|
|
container.innerHTML = '';
|
|
|
|
PRESET_TAGS.forEach(tag => {
|
|
const isChecked = hiddenTags.some(ht => ht.toLowerCase() === tag.name.toLowerCase());
|
|
const item = document.createElement('div');
|
|
item.className = 'preset-tag-item';
|
|
|
|
const checkbox = document.createElement('input');
|
|
checkbox.type = 'checkbox';
|
|
checkbox.className = 'preset-tag-checkbox';
|
|
checkbox.dataset.tag = tag.name;
|
|
checkbox.checked = isChecked;
|
|
|
|
const info = document.createElement('div');
|
|
info.className = 'preset-tag-info';
|
|
|
|
const name = document.createElement('div');
|
|
name.className = 'preset-tag-name';
|
|
name.textContent = tag.name;
|
|
|
|
const desc = document.createElement('div');
|
|
desc.className = 'preset-tag-desc';
|
|
desc.textContent = tag.desc;
|
|
|
|
info.appendChild(name);
|
|
info.appendChild(desc);
|
|
item.appendChild(checkbox);
|
|
item.appendChild(info);
|
|
container.appendChild(item);
|
|
});
|
|
|
|
container.querySelectorAll('.preset-tag-checkbox').forEach(cb => {
|
|
cb.addEventListener('change', function() {
|
|
const tagName = this.dataset.tag;
|
|
let hiddenTags = getHiddenTags();
|
|
|
|
if (this.checked) {
|
|
if (!hiddenTags.includes(tagName)) hiddenTags.push(tagName);
|
|
} else {
|
|
hiddenTags = hiddenTags.filter(t => t.toLowerCase() !== tagName.toLowerCase());
|
|
}
|
|
saveHiddenTags(hiddenTags);
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderCustomTags() {
|
|
const container = document.getElementById('custom-tags-list');
|
|
if (!container) return;
|
|
|
|
const hiddenTags = getHiddenTags();
|
|
const customTags = hiddenTags.filter(tag =>
|
|
!PRESET_TAGS.some(pt => pt.name.toLowerCase() === tag.toLowerCase())
|
|
);
|
|
|
|
container.innerHTML = '';
|
|
|
|
if (customTags.length === 0) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'no-custom-tags';
|
|
empty.textContent = '{'No custom hidden tags'|t}';
|
|
container.appendChild(empty);
|
|
return;
|
|
}
|
|
|
|
customTags.forEach(tag => {
|
|
const chip = document.createElement('div');
|
|
chip.className = 'custom-tag-chip';
|
|
|
|
const label = document.createElement('span');
|
|
label.textContent = tag;
|
|
|
|
const remove = document.createElement('span');
|
|
remove.className = 'remove-tag';
|
|
remove.dataset.tag = tag;
|
|
remove.textContent = '\u00D7';
|
|
|
|
chip.appendChild(label);
|
|
chip.appendChild(remove);
|
|
container.appendChild(chip);
|
|
});
|
|
|
|
container.querySelectorAll('.remove-tag').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const tagName = this.dataset.tag;
|
|
let hiddenTags = getHiddenTags();
|
|
hiddenTags = hiddenTags.filter(t => t.toLowerCase() !== tagName.toLowerCase());
|
|
saveHiddenTags(hiddenTags);
|
|
renderCustomTags();
|
|
});
|
|
});
|
|
}
|
|
|
|
function addCustomTag() {
|
|
const input = document.getElementById('custom-tag-input');
|
|
if (!input) return;
|
|
|
|
const tagName = input.value.trim().toLowerCase();
|
|
if (!tagName) return;
|
|
|
|
// Check if already exists
|
|
const hiddenTags = getHiddenTags();
|
|
if (hiddenTags.some(t => t.toLowerCase() === tagName)) {
|
|
input.value = '';
|
|
return;
|
|
}
|
|
|
|
hiddenTags.push(tagName);
|
|
saveHiddenTags(hiddenTags);
|
|
input.value = '';
|
|
renderCustomTags();
|
|
}
|
|
|
|
function resetToDefaults() {
|
|
if (confirm('Reset hidden tags to default settings?')) {
|
|
saveHiddenTags([...DEFAULT_HIDDEN_TAGS]);
|
|
renderPresetTags();
|
|
renderCustomTags();
|
|
}
|
|
}
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const toggleBtn = document.getElementById('hidden-tags-toggle');
|
|
const panel = document.getElementById('hidden-tags-panel');
|
|
const closeBtn = document.getElementById('close-hidden-tags');
|
|
const addBtn = document.getElementById('add-custom-tag');
|
|
const resetBtn = document.getElementById('reset-hidden-tags');
|
|
const customInput = document.getElementById('custom-tag-input');
|
|
|
|
if (toggleBtn && panel) {
|
|
toggleBtn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
panel.style.display = panel.style.display === 'none' ? '' : 'none';
|
|
if (panel.style.display !== 'none') {
|
|
renderPresetTags();
|
|
renderCustomTags();
|
|
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
});
|
|
}
|
|
|
|
if (closeBtn && panel) {
|
|
closeBtn.addEventListener('click', function() {
|
|
panel.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
if (addBtn) {
|
|
addBtn.addEventListener('click', addCustomTag);
|
|
}
|
|
|
|
if (customInput) {
|
|
customInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
addCustomTag();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (resetBtn) {
|
|
resetBtn.addEventListener('click', resetToDefaults);
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
|
|
</html> |