84 lines
23 KiB
HTML
84 lines
23 KiB
HTML
{% load static i18n %}
|
||
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>my2dos</title>
|
||
<meta name="theme-color" content="#212529">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-title" content="my2dos">
|
||
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
|
||
<link rel="icon" href="{% static 'icon.svg' %}" type="image/svg+xml">
|
||
<link rel="apple-touch-icon" href="{% static 'icon-192.png' %}">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||
<style>
|
||
body{background:linear-gradient(180deg,#f8fafc 0,#f7f7f4 18rem);color:#1f2937}.shell{max-width:960px}.navbar{box-shadow:0 .35rem 1.25rem rgba(15,23,42,.06)}.navbar a{text-decoration:none}.nav-soft{color:#475569!important;font-weight:500;padding:.2rem .35rem;border-radius:.5rem}.nav-soft:hover{color:#0d6efd!important;background:#eff6ff}.user-menu-btn{min-width:9rem}.card{border:0}.item-card{border:0;border-radius:1.1rem;box-shadow:0 .45rem 1.4rem rgba(15,23,42,.07)!important}.filter-panel{padding:.25rem .15rem}.border{border-color:rgba(15,23,42,.10)!important}.text-bg-light{background:#f1f5f9!important;color:#475569!important}.btn{border-radius:.7rem}.form-control,.form-select{border-color:#e2e8f0;border-radius:.75rem}.content p:last-child{margin-bottom:0}.content h1{font-size:1.75rem}.content h2{font-size:1.5rem}.content h3{font-size:1.25rem}.content h4,.content h5,.content h6{font-size:1rem;font-weight:700}.tag{font-size:.85rem}.slash-menu{position:absolute;z-index:20;top:100%;left:0;width:18rem}.slash-group-header{padding:.35rem .75rem .2rem;background:#f8fafc;color:#94a3b8;font-size:.65rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;border-top:1px solid #eef2f7}.slash-menu>div:first-of-type .slash-group-header{border-top:0}.slash-menu>div>.list-group-item{border-left:0;border-right:0;border-radius:0}.composer-slash-menu{position:relative;top:auto;left:auto;width:100%;max-width:28rem;margin-top:.25rem;overflow:hidden;border-radius:.75rem}.format-toolbar{position:relative;display:flex;align-items:center}.format-options{position:absolute;z-index:19;left:0;top:calc(100% + .35rem);display:flex;align-items:center;gap:.2rem;padding:.35rem;background:white;border:1px solid #e2e8f0;border-radius:.75rem}.format-options .btn{min-width:2rem}.format-divider{width:1px;height:1.5rem;background:#e2e8f0;margin:0 .15rem}.collection-document{max-width:800px;margin:0 auto}.collection-toc{padding:1rem 1.25rem;border-left:3px solid #e2e8f0;background:rgba(248,250,252,.7)}.collection-flow-section{margin-bottom:3rem;scroll-margin-top:5rem}.collection-flow-section:last-child{margin-bottom:0}.done{opacity:.6}.done .content{text-decoration:line-through}.content img{max-width:100%;max-height:420px;border-radius:.5rem;border:1px solid #ddd;padding:.25rem;background:white}.overview-content{max-height:calc(var(--overview-lines) * 1.55em);overflow:hidden}.overview-content.expanded{max-height:none}.content{min-width:0;max-width:100%;overflow-wrap:anywhere}.content pre{position:relative;display:block;width:100%;max-width:100%;box-sizing:border-box;background:#1f2937;color:#f9fafb;border-radius:.6rem;padding:1rem;overflow-x:auto;overflow-y:auto;white-space:pre}.content pre code{display:block;color:inherit;white-space:pre;min-width:0}.copy-code{position:absolute;top:.4rem;right:.4rem;line-height:1}.copy-item{position:absolute;right:.75rem;bottom:.5rem;line-height:1;opacity:0;transition:opacity .15s}.item-card:hover .copy-item,.copy-item:focus{opacity:.7}.copy-item:hover{opacity:1!important}.mobile-create-buttons,.mobile-create-nav,.mobile-tags,.mobile-media,.mobile-tag-picker,.mobile-bottom-nav{display:none}.mobile-tag-popup{position:fixed;inset:auto .75rem 1rem .75rem;z-index:1050;max-height:55vh;overflow:auto}.mobile-tag-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.25);z-index:1040}.camera-input{max-width:13rem}@media (max-width: 767.98px){body{padding-bottom:4.25rem}.shell{max-width:100%}main.container{padding:.25rem .75rem 1rem}.navbar .container{gap:.5rem}.navbar-brand{font-size:1rem}.navbar .d-flex{gap:.5rem!important}.user-menu-btn{min-width:auto;max-width:9rem}.item-card{border-radius:.75rem}.item-actions{width:100%;justify-content:stretch!important}.item-action-btn{flex:1 1 0;min-width:0}.mobile-create-buttons,.mobile-tags,.mobile-media{display:flex!important}.mobile-bottom-nav{display:flex!important;position:fixed;left:.75rem;right:.75rem;bottom:.75rem;z-index:1060;background:white;border-radius:1rem;box-shadow:0 .5rem 1.5rem rgba(15,23,42,.18);padding:.35rem}.mobile-bottom-nav .btn{font-size:.8rem;padding:.45rem .25rem}.mobile-tag-picker{display:block!important}.desktop-quick-create{display:none!important}.mobile-create-nav{display:none!important}.desktop-file{display:none!important}.mobile-create-buttons{position:sticky;top:57px;z-index:10;background:#f7f7f4;padding:.35rem 0}.toolbar-filter{display:block;background:#f7f7f4;border-top:4px solid #fff;padding:.35rem .75rem .5rem;box-shadow:0 .4rem .9rem rgba(15,23,42,.05)}.mobile-filter-sidebar{display:none}.filter-panel{background:#f7f7f4;padding:0}.filter-panel-title{display:block;margin:0 0 .3rem!important;font-size:.82rem;color:#64748b;line-height:1}.filter-row{display:grid;grid-template-columns:repeat(5,minmax(0,1fr)) 2.25rem;gap:.25rem;width:100%}.filter-row .btn,.filter-row summary{width:100%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-left:.1rem;padding-right:.1rem}.mobile-filter-toggle{position:absolute;opacity:0;pointer-events:none}.mobile-filter-more{grid-column:6;grid-row:1;position:relative;border:0;background:transparent;color:#475569;font-size:1.2rem;line-height:1;text-align:center;cursor:pointer}.mobile-filter-more .badge{position:absolute;top:-.15rem;right:.15rem;font-size:.55rem;line-height:1;padding:.12rem .25rem}.mobile-filter-details{display:none;grid-column:1/-1;grid-row:2;margin-top:.45rem;padding:.75rem;background:white;border-radius:1rem;box-shadow:0 .35rem 1rem rgba(15,23,42,.12)}.mobile-filter-toggle:checked ~ .mobile-filter-details{display:block}.quick-actions{flex-wrap:wrap}.quick-actions .form-control{min-width:100%}.slash-menu{width:calc(100vw - 1.5rem)}.composer-slash-menu{width:100%}textarea.form-control-lg{font-size:1rem}.btn{touch-action:manipulation}.item-row{flex-direction:column}.item-actions{align-self:flex-end}.item-main{width:100%;min-width:0}.copy-item{opacity:.65}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="top-toolbar sticky-top">
|
||
<nav class="navbar bg-white"><div class="container shell"><div class="d-flex align-items-center gap-2 w-100">
|
||
<div class="d-none d-md-flex align-items-center gap-2">
|
||
<a class="small nav-soft fw-semibold" href="/">{% for label in nav_overview_labels %}{{ label }}{% if not forloop.last %} · {% endif %}{% endfor %}</a>
|
||
{% for link in nav_hidden_links %}<a class="small nav-soft" href="{{ link.url }}">{{ link.label }}</a>{% endfor %}
|
||
{% if user.is_authenticated %}<a class="small nav-soft" href="{% url 'journal' %}">{% trans "Journal" %}</a><a class="small nav-soft" href="{% url 'kanban_list' %}">{% trans "Kanban" %}</a><a class="small nav-soft" href="{% url 'collection_list' %}">{% trans "Collections" %}</a><a class="small nav-soft" href="{% url 'team_list' %}">{% trans "Teams" %}</a>{% endif %}
|
||
</div>
|
||
<div class="d-flex d-md-none align-items-center gap-1"><a class="small nav-soft fw-semibold" href="/">Start</a>{% if user.is_authenticated %}<a class="small nav-soft" href="{% url 'journal' %}">{% trans "Journal" %}</a><a class="small nav-soft" href="{% url 'team_list' %}">{% trans "Teams" %}</a>{% endif %}</div>
|
||
{% if user.is_authenticated %}
|
||
<div class="dropdown ms-auto"><button class="btn btn-sm btn-outline-secondary dropdown-toggle user-menu-btn" data-bs-toggle="dropdown">{{ active_workspace.name|default:user.get_full_name|default:user.username }}</button><ul class="dropdown-menu dropdown-menu-end">
|
||
<li><h6 class="dropdown-header">{% trans "Workspace" %}</h6></li>
|
||
{% for membership in workspace_memberships %}<li><form method="post" action="{% url 'workspace_switch' membership.workspace.id %}">{% csrf_token %}<input type="hidden" name="next" value="{{ request.get_full_path }}"><button class="dropdown-item d-flex justify-content-between gap-2 {% if active_workspace.id == membership.workspace.id %}active{% endif %}"><span>{% if active_workspace.id == membership.workspace.id %}✓ {% endif %}{{ membership.workspace.name }}</span>{% if membership.due_count %}<span class="badge text-bg-danger">{{ membership.due_count }}</span>{% endif %}</button></form></li>{% endfor %}
|
||
<li><a class="dropdown-item" href="{% url 'settings' %}#workspaces">+ {% trans "Create workspace" %}</a></li>
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li><a class="dropdown-item" href="{% url 'settings' %}">{% trans "Settings" %}</a></li><li><button id="pwa-install" type="button" class="dropdown-item d-none">{% trans "Install app" %}</button></li>{% if user.is_staff %}<li><a class="dropdown-item" href="/admin/">Admin</a></li>{% endif %}<li><hr class="dropdown-divider"></li><li><form method="post" action="{% url 'logout' %}">{% csrf_token %}<button class="dropdown-item">{% trans "Logout" %}</button></form></li></ul></div>
|
||
{% if 'IchWillHeimWebView' in request.META.HTTP_USER_AGENT %}<a class="btn btn-sm btn-outline-danger" href="iwh://close" title="{% trans 'Close' %}" aria-label="{% trans 'Close' %}">×</a>{% endif %}
|
||
{% else %}<a class="btn btn-sm btn-dark ms-auto" href="{% url 'login' %}">{% trans "Login" %}</a>{% endif %}
|
||
</div></div></nav>
|
||
{% block toolbar_filter %}{% endblock %}
|
||
</div>
|
||
<main class="container shell py-4">{% for message in messages %}<div class="alert alert-{{ message.tags|default:'info' }}">{{ message }}</div>{% endfor %}{% block content %}{% endblock %}</main>
|
||
<div id="image-lightbox" class="position-fixed top-0 start-0 w-100 h-100 d-none align-items-center justify-content-center p-3" style="z-index:2000;background:rgba(15,23,42,.72)" role="dialog" aria-modal="true" aria-label="{% trans 'Image preview' %}">
|
||
<button type="button" class="btn btn-light position-absolute top-0 end-0 m-3" data-lightbox-close aria-label="{% trans 'Close' %}">×</button>
|
||
<img class="img-fluid bg-white rounded shadow" style="max-height:90vh;max-width:95vw" alt="">
|
||
</div>
|
||
{% if user.is_authenticated and request.resolver_match.url_name == 'index' %}<nav class="mobile-bottom-nav gap-1 d-md-none" aria-label="Mobile navigation"><a class="btn btn-primary flex-fill" href="{% url 'new_item' %}?kind=todo">Aufgabe</a><a class="btn btn-secondary flex-fill" href="{% url 'new_item' %}?kind=note">Notiz</a><a class="btn btn-success flex-fill" href="{% url 'new_item' %}?kind=link">Link</a><a class="btn btn-info flex-fill" href="{% url 'new_item' %}?kind=journal">Journal</a></nav>{% endif %}
|
||
<script>
|
||
document.body.addEventListener('htmx:configRequest', e => {e.detail.headers['X-CSRFToken']='{{ csrf_token }}'});
|
||
window.quickComposer = window.quickComposer || function(tags, itemId=null){
|
||
return {
|
||
open:false,showTags:false,showFormatting:false,text:'',active:0,query:'',queryStart:0,currentEl:null,preview:null,pendingFiles:[],linkSuggestions:[],userSuggestions:[],attachmentSuggestions:[],itemId:itemId,tags:tags||[],
|
||
init(){document.body.addEventListener('tagsChanged',()=>{fetch('/api/tags/').then(r=>r.json()).then(d=>{this.tags=d.tags||[]})})},
|
||
commands:[{token:'/todo',label:'{% trans "Create task" %}',group:'{% trans "Type" %}'},{token:'/note',label:'{% trans "Create note" %}',group:'{% trans "Type" %}'},{token:'/link',label:'{% trans "Save link" %}',group:'{% trans "Type" %}'},{token:'/journal',label:'{% trans "Journal entry" %}',group:'{% trans "Type" %}'},{token:'/public',label:'{% trans "public" %}',group:'{% trans "Visibility" %}'},{token:'/private',label:'{% trans "private" %}',group:'{% trans "Visibility" %}'},{token:'/datum',label:'{% trans "Due date (DD.MM.YY)" %}',group:'{% trans "Date" %}'},{token:'/morgen',label:'{% trans "Tomorrow" %}',group:'{% trans "Date" %}'},{token:'/übermorgen',label:'{% trans "Day after tomorrow" %}',group:'{% trans "Date" %}'},{token:'/nächstewoche',label:'{% trans "Next week" %}',group:'{% trans "Date" %}'},{token:'/code',label:'{% trans "Insert Markdown code block" %}',group:'{% trans "Formatting" %}'},{token:'/image',label:'{% trans "Insert uploaded image" %}',group:'{% trans "References" %}'},{token:'[[',label:'{% trans "Search internal task/note" %}',group:'{% trans "References" %}'},{token:'![[',label:'{% trans "Insert image from attachments" %}',group:'{% trans "References" %}'}],
|
||
get filtered(){let q=this.query.toLowerCase();let selectedType=(this.text.match(/^\/(todo|note|link|journal)(?=\s)/i)||[])[1]?.toLowerCase();let typeTokens=['/todo','/note','/link','/journal'];let dateTokens=['/datum','/morgen','/übermorgen','/nächstewoche'];let commands=this.commands.filter(c=>{if(typeTokens.includes(c.token))return !selectedType&&this.queryStart===0;if(this.queryStart===0&&!selectedType)return false;if(selectedType&&selectedType!=='todo'&&dateTokens.includes(c.token))return false;return true});let list=q.startsWith('#')?this.tags.filter(t=>('#'+t).toLowerCase().startsWith(q)).map(t=>({token:'#'+t,label:'Tag',group:'{% trans "Tags" %}'})):q.startsWith('@')?this.userSuggestions.map(u=>({token:'@'+u.username,label:u.label,group:'{% trans "Users" %}'})):q.startsWith('![[')?(this.itemId?this.attachmentSuggestions.map(a=>({token:`![[file:${a.id}]]`,label:`{% trans "Image:" %} ${a.name}`,group:'{% trans "References" %}'})):this.pendingFiles.map(f=>({token:`![[paste:${f.name}]]`,label:`Bild: ${f.name}`,group:'{% trans "References" %}'}))):q.startsWith('[[')?this.linkSuggestions.map(i=>({token:`[[${i.id}]]`,label:`${i.kind}: ${i.label}`,group:'{% trans "References" %}'})):commands.filter(c=>!q||c.token.toLowerCase().startsWith(q));return list.length?list:[{token:this.query,label:'neu',group:''}]},
|
||
isContentField(el){return el && (el.name==='content' || el.id==='id_content')},
|
||
onInput(e){this.update(e.target)},
|
||
handlePaste(e){let item=[...(e.clipboardData?.items||[])].find(i=>i.kind==='file'&&i.type.startsWith('image/'));if(!item||!this.$refs.fileInput)return;let file=item.getAsFile();let ext=(file.type.split('/')[1]||'png').replace('jpeg','jpg');let named=new File([file],`paste-${new Date().toISOString().replace(/[:.]/g,'-')}.${ext}`,{type:file.type});let dt=new DataTransfer();[...(this.$refs.fileInput.files||[])].forEach(f=>dt.items.add(f));dt.items.add(named);this.$refs.fileInput.files=dt.files;this.pendingFiles=[...dt.files].filter(f=>f.type.startsWith('image/'));this.preview=URL.createObjectURL(named)},
|
||
insertImageToken(){if(this.pendingFiles.length)this.insert(`![[paste:${this.pendingFiles[this.pendingFiles.length-1].name}]]`)},
|
||
previewFile(e){this.pendingFiles=[...e.target.files].filter(f=>f.type.startsWith('image/'));let file=this.pendingFiles[0];this.preview=file?URL.createObjectURL(file):null},
|
||
onKeydown(e){this.currentEl=e.target;let shortcut=(e.ctrlKey||e.metaKey)&&{b:'bold',i:'italic',u:'underline'}[e.key.toLowerCase()];if(shortcut){e.preventDefault();this.formatSelection(shortcut);return}if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){e.preventDefault();this.open=false;e.target.form.requestSubmit();return}if(this.open&&e.key==='ArrowDown'){e.preventDefault();this.active=(this.active+1)%this.filtered.length;return}if(this.open&&e.key==='ArrowUp'){e.preventDefault();this.active=(this.active-1+this.filtered.length)%this.filtered.length;return}if(this.open&&(e.key==='Enter'||e.key==='Tab')&&!e.shiftKey){e.preventDefault();this.insert(this.filtered[this.active].token);return}if(e.key==='Escape'){this.open=false;return}if(e.key===' '){this.open=false;return}setTimeout(()=>this.update(e.target),0)},
|
||
update(el){this.currentEl=el;if(this.isContentField(el))this.text=el.value;let before=el.value.slice(0,el.selectionStart);let fragment=(before.match(/(^|\s)([^\s]*)$/)||['','',''])[2];this.query=fragment;this.queryStart=el.selectionStart-fragment.length;this.active=0;this.open=fragment.startsWith('/')||fragment.startsWith('#')||fragment.startsWith('@')||fragment.startsWith('[[')||fragment.startsWith('![[');if(fragment.startsWith('@')){fetch(`/api/user-suggestions/?q=${encodeURIComponent(fragment.slice(1))}`).then(r=>r.json()).then(d=>{this.userSuggestions=d.users||[]})}else if(fragment.startsWith('![[')&&this.itemId){fetch(`/api/attachment-suggestions/?item=${this.itemId}`).then(r=>r.json()).then(d=>{this.attachmentSuggestions=d.attachments||[]})}else if(fragment.startsWith('[[')){fetch(`/api/item-suggestions/?q=${encodeURIComponent(fragment.slice(2))}`).then(r=>r.json()).then(d=>{this.linkSuggestions=d.items||[]})}},
|
||
formatSelection(kind){let el=this.currentEl&&this.currentEl.matches('textarea')?this.currentEl:this.$el.querySelector('textarea[name="content"],textarea');if(!el)return;let start=el.selectionStart??0,end=el.selectionEnd??start;let selected=el.value.slice(start,end);let wrappers={bold:['**','**','Text'],italic:['*','*','Text'],underline:['<u>','</u>','Text'],code:['`','`','Code']};let value,newStart,newEnd;if(wrappers[kind]){let [before,after,placeholder]=wrappers[kind];let inner=selected||placeholder;value=el.value.slice(0,start)+before+inner+after+el.value.slice(end);newStart=start+before.length;newEnd=newStart+inner.length}else{let prefixes={heading:'## ',quote:'> ',list:'- ',numbered:'1. '};let prefix=prefixes[kind]||'';let inner=selected||'{% trans "Text" %}';let formatted=inner.split('\n').map(line=>prefix+line).join('\n');value=el.value.slice(0,start)+formatted+el.value.slice(end);newStart=start;newEnd=start+formatted.length}el.value=value;if(this.isContentField(el))this.text=value;this.$nextTick(()=>{el.focus();el.setSelectionRange(newStart,newEnd)})},
|
||
addToken(token){let el=this.currentEl||this.$el.querySelector('textarea');if(!el)return;let pos=el.selectionStart??el.value.length;let before=el.value.slice(0,pos);let after=el.value.slice(pos);let prefix=before&& !before.endsWith(' ') && !before.endsWith('\n')?' ':'';let value=before+prefix+token+' '+after;if(this.isContentField(el))this.text=value;el.value=value;this.showTags=false;this.$nextTick(()=>{let p=before.length+prefix.length+token.length+1;el.focus();el.setSelectionRange(p,p)})},
|
||
insert(token){let el=this.currentEl||this.$el.querySelector('textarea');if(!el)return;let isCode=token==='/code';let isImage=token==='/image';if(isCode)token='```\n\n```';if(isImage)token=this.pendingFiles.length?`![[paste:${this.pendingFiles[this.pendingFiles.length-1].name}]]`:'![[';let pos=el.selectionStart;let before=el.value.slice(0,pos);let after=el.value.slice(pos);let match=before.match(/(^|\s)([^\s]*)$/);let start=match?pos-match[2].length:pos;let needsPrefix=start>0&&el.value[start-1]!=='\n'&&el.value[start-1]!==' ';let prefix=isCode?(start>0&&el.value[start-1]!=='\n'?'\n':''):(needsPrefix?' ':'');let suffix=(isCode||token==='![[')?'':' ';let value=el.value.slice(0,start)+prefix+token+suffix+after;if(this.isContentField(el))this.text=value;el.value=value;this.open=false;this.$nextTick(()=>{let p=isCode?start+prefix.length+4:start+prefix.length+token.length+suffix.length;el.focus();el.setSelectionRange(p,p)})}
|
||
}
|
||
};
|
||
window.searchBox = function(tags){return{open:false,active:0,q:'',tags:tags||[],get filtered(){let q=this.q.toLowerCase();return this.tags.filter(t=>('#'+t).toLowerCase().startsWith(q))},onInput(e){this.q=e.target.value;this.open=this.q.startsWith('#')},onKeydown(e){if(!this.open)return;if(e.key==='ArrowDown'){e.preventDefault();this.active=(this.active+1)%Math.max(this.filtered.length,1)}else if(e.key==='ArrowUp'){e.preventDefault();this.active=(this.active-1+Math.max(this.filtered.length,1))%Math.max(this.filtered.length,1)}else if(e.key==='Enter'||e.key==='Tab'){if(this.filtered.length){e.preventDefault();this.select(this.filtered[this.active])}}else if(e.key==='Escape'){this.open=false}},select(tag){let url=new URL(window.location.href);url.searchParams.set('tag',tag);url.searchParams.delete('q');window.location.href=url.pathname+'?'+url.searchParams.toString()}}}
|
||
async function writeClipboard(text){if(navigator.clipboard&&window.isSecureContext){return navigator.clipboard.writeText(text)}let area=document.createElement('textarea');area.value=text;area.style.position='fixed';area.style.opacity='0';document.body.appendChild(area);area.select();document.execCommand('copy');area.remove()}
|
||
function copyItemText(button,text){writeClipboard(text).then(()=>{button.innerHTML='✓';setTimeout(()=>button.innerHTML='⧉',1200)})}
|
||
function enhanceCodeBlocks(root=document){root.querySelectorAll('.content pre:not([data-copy])').forEach(pre=>{pre.dataset.copy='1';let btn=document.createElement('button');btn.type='button';btn.className='copy-code btn btn-sm btn-light';btn.title='Kopieren';btn.innerHTML='⧉';btn.addEventListener('click',()=>writeClipboard(pre.querySelector('code')?.innerText || pre.innerText).then(()=>{btn.innerHTML='✓';setTimeout(()=>btn.innerHTML='⧉',1200)}));pre.appendChild(btn)})}
|
||
document.addEventListener('DOMContentLoaded',()=>enhanceCodeBlocks());document.body.addEventListener('htmx:afterSwap',e=>enhanceCodeBlocks(e.target));
|
||
function closeImageLightbox(){let box=document.getElementById('image-lightbox');if(box)box.classList.add('d-none')}
|
||
document.addEventListener('click', event => {let trigger=event.target.closest('.attachment-image-preview');if(trigger){let box=document.getElementById('image-lightbox');let img=box?.querySelector('img');if(box&&img){img.src=trigger.dataset.imageUrl;img.alt=trigger.dataset.imageName||'';box.classList.remove('d-none');box.classList.add('d-flex')}return}let box=event.target.closest('#image-lightbox');if(box&&(event.target===box||event.target.closest('[data-lightbox-close]')))closeImageLightbox()});
|
||
document.addEventListener('keydown', event => {if(event.key==='Escape')closeImageLightbox()});
|
||
document.addEventListener('click', event => {let link=event.target.closest('a.tag-filter[data-tag]');if(!link||!(event.ctrlKey||event.metaKey))return;event.preventDefault();let url=new URL(window.location.href);let tag=link.dataset.tag;let tags=url.searchParams.getAll('tag');url.searchParams.delete('tag');if(tags.includes(tag)){tags=tags.filter(t=>t!==tag)}else{tags.push(tag)}tags.forEach(t=>url.searchParams.append('tag',t));window.location.href=url.pathname+(url.searchParams.toString()?'?'+url.searchParams.toString():'')});
|
||
let deferredPwaPrompt=null;window.addEventListener('beforeinstallprompt',event=>{event.preventDefault();deferredPwaPrompt=event;document.getElementById('pwa-install')?.classList.remove('d-none')});document.addEventListener('click',async event=>{if(event.target?.id==='pwa-install'&&deferredPwaPrompt){deferredPwaPrompt.prompt();await deferredPwaPrompt.userChoice;deferredPwaPrompt=null;event.target.classList.add('d-none')}});
|
||
if('serviceWorker' in navigator){window.addEventListener('load',()=>navigator.serviceWorker.register('/sw.js').catch(()=>{}));}
|
||
</script>
|
||
</body>
|
||
</html>
|