diff options
author | Arno Richter <oelna@oelna.de> | 2022-12-21 15:05:28 +0100 |
---|---|---|
committer | Arno Richter <oelna@oelna.de> | 2022-12-21 15:05:28 +0100 |
commit | 482fd7adee5e9e0990bf5904ed7d754d315de649 (patch) | |
tree | 7523a6f3482b6cb024624310f21fa7eeb05e9866 | |
parent | 057cace8b32e6c3d105695b517eae262071601f4 (diff) | |
download | microblog-482fd7adee5e9e0990bf5904ed7d754d315de649.tar.gz microblog-482fd7adee5e9e0990bf5904ed7d754d315de649.tar.bz2 microblog-482fd7adee5e9e0990bf5904ed7d754d315de649.zip |
first attempt at image attachments!
-rw-r--r-- | .htaccess | 2 | ||||
-rw-r--r-- | css/microblog.css | 107 | ||||
-rw-r--r-- | favicon.ico | bin | 0 -> 318 bytes | |||
-rw-r--r-- | files/2022/12/10-c067078.png | bin | 0 -> 94005 bytes | |||
-rw-r--r-- | files/2022/12/12-da0864c.jpg | bin | 0 -> 72022 bytes | |||
-rw-r--r-- | files/2022/12/13-1612d57.jpg | bin | 0 -> 49606 bytes | |||
-rw-r--r-- | files/2022/12/13-8b2031d.jpg | bin | 0 -> 4243895 bytes | |||
-rw-r--r-- | files/2022/12/13-eae0c26.gif | bin | 0 -> 2044842 bytes | |||
-rw-r--r-- | files/2022/12/6-1612d57.jpg | bin | 0 -> 49606 bytes | |||
-rw-r--r-- | files/2022/12/6-426ace2.gif | bin | 0 -> 15975 bytes | |||
-rw-r--r-- | files/2022/12/6-da0864c.jpg | bin | 0 -> 72022 bytes | |||
-rw-r--r-- | files/2022/12/6-eae0c26.gif | bin | 0 -> 2044842 bytes | |||
-rw-r--r-- | files/2022/12/7-435f1c5.jpg | bin | 0 -> 146075 bytes | |||
-rw-r--r-- | files/2022/12/7-597815d.jpg | bin | 0 -> 63967 bytes | |||
-rw-r--r-- | files/2022/12/7-5e1af8d.jpg | bin | 0 -> 18883 bytes | |||
-rw-r--r-- | files/2022/12/7-98e0953.png | bin | 0 -> 8103 bytes | |||
-rw-r--r-- | files/2022/12/7-da0864c.jpg | bin | 0 -> 72022 bytes | |||
-rw-r--r-- | files/2022/12/8-20572ca.jpg | bin | 0 -> 157923 bytes | |||
-rw-r--r-- | files/2022/12/8-5e1af8d.jpg | bin | 0 -> 18883 bytes | |||
-rw-r--r-- | files/2022/12/8-da0864c.jpg | bin | 0 -> 72022 bytes | |||
-rw-r--r-- | files/2022/12/81-5e1af8d.jpg | bin | 0 -> 18883 bytes | |||
-rw-r--r-- | files/2022/12/81-da0864c.jpg | bin | 0 -> 72022 bytes | |||
-rw-r--r-- | files/2022/12/82-506df72.jpg | bin | 0 -> 3020458 bytes | |||
-rw-r--r-- | files/2022/12/83-5e1af8d.jpg | bin | 0 -> 18883 bytes | |||
-rw-r--r-- | files/2022/12/83-da0864c.jpg | bin | 0 -> 72022 bytes | |||
-rw-r--r-- | files/2022/12/84-5e1af8d.jpg | bin | 0 -> 18883 bytes | |||
-rw-r--r-- | files/2022/12/85-5e1af8d.jpg | bin | 0 -> 18883 bytes | |||
-rw-r--r-- | files/2022/12/85-da0864c.jpg | bin | 0 -> 72022 bytes | |||
-rw-r--r-- | files/2022/12/86-da0864c.jpg | bin | 0 -> 72022 bytes | |||
-rw-r--r-- | files/2022/12/87-da0864c.jpg | bin | 0 -> 72022 bytes | |||
-rw-r--r-- | files/2022/12/88-5e1af8d.jpg | bin | 0 -> 18883 bytes | |||
-rw-r--r-- | files/2022/12/89-5e1af8d.jpg | bin | 0 -> 18883 bytes | |||
-rw-r--r-- | files/2022/12/9-5e1af8d.jpg | bin | 0 -> 18883 bytes | |||
-rw-r--r-- | files/2022/12/9-da0864c.jpg | bin | 0 -> 72022 bytes | |||
-rw-r--r-- | index.php | 57 | ||||
-rw-r--r-- | js/microblog.js | 193 | ||||
-rw-r--r-- | js/squircle.js | 146 | ||||
-rw-r--r-- | lib/database.php | 48 | ||||
-rw-r--r-- | lib/functions.php | 268 | ||||
-rw-r--r-- | lib/xmlrpc.php | 4 | ||||
-rw-r--r-- | microblog.js | 21 | ||||
-rw-r--r-- | snippets/header.snippet.php | 11 | ||||
-rw-r--r-- | templates/postform.inc.php | 19 | ||||
-rw-r--r-- | templates/single.inc.php | 89 | ||||
-rw-r--r-- | templates/timeline.inc.php | 30 |
45 files changed, 929 insertions, 66 deletions
@@ -19,8 +19,6 @@ RewriteEngine On RewriteBase /microblog # friendly URLs -RewriteRule ^rsd/?$ lib/rsd.xml.php [L] -RewriteRule ^xmlrpc/?(.*)$ lib/xmlrpc.php?/$1 [L] RewriteRule ^feed/json/?$ feed/feed.json [L] RewriteRule ^feed/atom/?$ feed/feed.xml [L] diff --git a/css/microblog.css b/css/microblog.css index 1bc41be..e6a657a 100644 --- a/css/microblog.css +++ b/css/microblog.css @@ -14,6 +14,12 @@ html { color: var(--text-color); } +img { + display: block; + max-width: 100%; + height: auto; +} + .wrap { width: min(95%, 40rem); margin: 2rem auto; @@ -37,6 +43,10 @@ html { background: coral; } +.hidden { + display: none !important; +} + nav.main ul { display: flex; margin-bottom: 2rem; @@ -191,6 +201,13 @@ form.edit, outline: none; } +:is(.postform, .edit) .post-nav { + width: 100%; + display: flex; + gap: 1rem; + align-items: center; +} + :is(.postform, .edit) input[type="submit"], .login input[type="submit"] { -webkit-appearance: none; @@ -209,10 +226,86 @@ form.edit, } :is(.postform, .edit) #count { - float: left; color: var(--background-color); } +:is(.postform, .edit) #post-droparea { + border: 0.15rem dashed rgba(0,0,0,0.2); + color: rgba(0,0,0,0.35); + padding: 0.25rem; + cursor: pointer; +} + +:is(.postform, .edit) #post-droparea.drag, +:is(.postform, .edit) #post-droparea:is(:hover, :focus) { + background-color: var(--primary-color); + color: #fff; + border: 0.15rem solid var(--primary-color); +} + +:is(.postform, .edit) #post-attachments-label { + display: flex; + border: 0.15rem dashed rgba(0,0,0,0.4); + color: rgba(0,0,0,0.4); + padding: 0.25rem; + cursor: pointer; + position: relative; + align-self: stretch; + align-items: center; +} + +:is(.postform, .edit) #post-attachments { + /* cover the entire label, for drag and drop */ + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; +} + +:is(.postform, .edit) #post-attachments-list { + flex: 1; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + padding-inline-end: 1rem; + align-self: stretch; + justify-content: center; +} + +:is(.postform, .edit) #post-attachments-list li + li { + margin-block-start: 0.25em; + border-top: 1px solid rgba(0,0,0,0.2); + padding-block-start: 0.25em; +} + +:is(.postform, .edit) #post-attachments-list img.file-preview { + display: inline-block; + vertical-align: middle; + margin-right: 1ch; + width: 1.75rem; + height: 1.75rem; + outline: 0px solid #f0f; + object-fit: cover; + background: #fff; +} + +:is(.postform, .edit) #post-attachments-list input[type="checkbox"] { + -webkit-appearance: checkbox; + appearance: checkbox; +} + +:is(.timeline, .single) .post-attachments { + grid-column-start: span 6; + margin-block-start: 1rem; +} + +:is(.timeline, .single) .post-attachments li + li { + margin-block-start: 0.5rem; +} + :is(.postform, .edit) .message, .login .message { background-color: #87b26c; @@ -264,3 +357,15 @@ footer li a { font-weight: bold; margin-bottom: 0.5rem; } + +/* +@supports (background: paint(id)) { + input[type="submit"] { + background: paint(squircle) !important; + --squircle-radius: 8px; + --squircle-fill: var(--primary-color); + + border-radius: 0; + } +} +*/ diff --git a/favicon.ico b/favicon.ico Binary files differnew file mode 100644 index 0000000..550d600 --- /dev/null +++ b/favicon.ico diff --git a/files/2022/12/10-c067078.png b/files/2022/12/10-c067078.png Binary files differnew file mode 100644 index 0000000..621b565 --- /dev/null +++ b/files/2022/12/10-c067078.png diff --git a/files/2022/12/12-da0864c.jpg b/files/2022/12/12-da0864c.jpg Binary files differnew file mode 100644 index 0000000..b4195d3 --- /dev/null +++ b/files/2022/12/12-da0864c.jpg diff --git a/files/2022/12/13-1612d57.jpg b/files/2022/12/13-1612d57.jpg Binary files differnew file mode 100644 index 0000000..0913630 --- /dev/null +++ b/files/2022/12/13-1612d57.jpg diff --git a/files/2022/12/13-8b2031d.jpg b/files/2022/12/13-8b2031d.jpg Binary files differnew file mode 100644 index 0000000..b2071b9 --- /dev/null +++ b/files/2022/12/13-8b2031d.jpg diff --git a/files/2022/12/13-eae0c26.gif b/files/2022/12/13-eae0c26.gif Binary files differnew file mode 100644 index 0000000..8e66b5c --- /dev/null +++ b/files/2022/12/13-eae0c26.gif diff --git a/files/2022/12/6-1612d57.jpg b/files/2022/12/6-1612d57.jpg Binary files differnew file mode 100644 index 0000000..0913630 --- /dev/null +++ b/files/2022/12/6-1612d57.jpg diff --git a/files/2022/12/6-426ace2.gif b/files/2022/12/6-426ace2.gif Binary files differnew file mode 100644 index 0000000..4f0665e --- /dev/null +++ b/files/2022/12/6-426ace2.gif diff --git a/files/2022/12/6-da0864c.jpg b/files/2022/12/6-da0864c.jpg Binary files differnew file mode 100644 index 0000000..b4195d3 --- /dev/null +++ b/files/2022/12/6-da0864c.jpg diff --git a/files/2022/12/6-eae0c26.gif b/files/2022/12/6-eae0c26.gif Binary files differnew file mode 100644 index 0000000..8e66b5c --- /dev/null +++ b/files/2022/12/6-eae0c26.gif diff --git a/files/2022/12/7-435f1c5.jpg b/files/2022/12/7-435f1c5.jpg Binary files differnew file mode 100644 index 0000000..9a52ca4 --- /dev/null +++ b/files/2022/12/7-435f1c5.jpg diff --git a/files/2022/12/7-597815d.jpg b/files/2022/12/7-597815d.jpg Binary files differnew file mode 100644 index 0000000..22a7820 --- /dev/null +++ b/files/2022/12/7-597815d.jpg diff --git a/files/2022/12/7-5e1af8d.jpg b/files/2022/12/7-5e1af8d.jpg Binary files differnew file mode 100644 index 0000000..a564f0e --- /dev/null +++ b/files/2022/12/7-5e1af8d.jpg diff --git a/files/2022/12/7-98e0953.png b/files/2022/12/7-98e0953.png Binary files differnew file mode 100644 index 0000000..ea839ff --- /dev/null +++ b/files/2022/12/7-98e0953.png diff --git a/files/2022/12/7-da0864c.jpg b/files/2022/12/7-da0864c.jpg Binary files differnew file mode 100644 index 0000000..b4195d3 --- /dev/null +++ b/files/2022/12/7-da0864c.jpg diff --git a/files/2022/12/8-20572ca.jpg b/files/2022/12/8-20572ca.jpg Binary files differnew file mode 100644 index 0000000..d93a2d6 --- /dev/null +++ b/files/2022/12/8-20572ca.jpg diff --git a/files/2022/12/8-5e1af8d.jpg b/files/2022/12/8-5e1af8d.jpg Binary files differnew file mode 100644 index 0000000..a564f0e --- /dev/null +++ b/files/2022/12/8-5e1af8d.jpg diff --git a/files/2022/12/8-da0864c.jpg b/files/2022/12/8-da0864c.jpg Binary files differnew file mode 100644 index 0000000..b4195d3 --- /dev/null +++ b/files/2022/12/8-da0864c.jpg diff --git a/files/2022/12/81-5e1af8d.jpg b/files/2022/12/81-5e1af8d.jpg Binary files differnew file mode 100644 index 0000000..a564f0e --- /dev/null +++ b/files/2022/12/81-5e1af8d.jpg diff --git a/files/2022/12/81-da0864c.jpg b/files/2022/12/81-da0864c.jpg Binary files differnew file mode 100644 index 0000000..b4195d3 --- /dev/null +++ b/files/2022/12/81-da0864c.jpg diff --git a/files/2022/12/82-506df72.jpg b/files/2022/12/82-506df72.jpg Binary files differnew file mode 100644 index 0000000..d0b1bfe --- /dev/null +++ b/files/2022/12/82-506df72.jpg diff --git a/files/2022/12/83-5e1af8d.jpg b/files/2022/12/83-5e1af8d.jpg Binary files differnew file mode 100644 index 0000000..a564f0e --- /dev/null +++ b/files/2022/12/83-5e1af8d.jpg diff --git a/files/2022/12/83-da0864c.jpg b/files/2022/12/83-da0864c.jpg Binary files differnew file mode 100644 index 0000000..b4195d3 --- /dev/null +++ b/files/2022/12/83-da0864c.jpg diff --git a/files/2022/12/84-5e1af8d.jpg b/files/2022/12/84-5e1af8d.jpg Binary files differnew file mode 100644 index 0000000..a564f0e --- /dev/null +++ b/files/2022/12/84-5e1af8d.jpg diff --git a/files/2022/12/85-5e1af8d.jpg b/files/2022/12/85-5e1af8d.jpg Binary files differnew file mode 100644 index 0000000..a564f0e --- /dev/null +++ b/files/2022/12/85-5e1af8d.jpg diff --git a/files/2022/12/85-da0864c.jpg b/files/2022/12/85-da0864c.jpg Binary files differnew file mode 100644 index 0000000..b4195d3 --- /dev/null +++ b/files/2022/12/85-da0864c.jpg diff --git a/files/2022/12/86-da0864c.jpg b/files/2022/12/86-da0864c.jpg Binary files differnew file mode 100644 index 0000000..b4195d3 --- /dev/null +++ b/files/2022/12/86-da0864c.jpg diff --git a/files/2022/12/87-da0864c.jpg b/files/2022/12/87-da0864c.jpg Binary files differnew file mode 100644 index 0000000..b4195d3 --- /dev/null +++ b/files/2022/12/87-da0864c.jpg diff --git a/files/2022/12/88-5e1af8d.jpg b/files/2022/12/88-5e1af8d.jpg Binary files differnew file mode 100644 index 0000000..a564f0e --- /dev/null +++ b/files/2022/12/88-5e1af8d.jpg diff --git a/files/2022/12/89-5e1af8d.jpg b/files/2022/12/89-5e1af8d.jpg Binary files differnew file mode 100644 index 0000000..a564f0e --- /dev/null +++ b/files/2022/12/89-5e1af8d.jpg diff --git a/files/2022/12/9-5e1af8d.jpg b/files/2022/12/9-5e1af8d.jpg Binary files differnew file mode 100644 index 0000000..a564f0e --- /dev/null +++ b/files/2022/12/9-5e1af8d.jpg diff --git a/files/2022/12/9-da0864c.jpg b/files/2022/12/9-da0864c.jpg Binary files differnew file mode 100644 index 0000000..b4195d3 --- /dev/null +++ b/files/2022/12/9-da0864c.jpg @@ -13,29 +13,40 @@ $template = 'single'; require_once(ROOT.DS.'templates'.DS.'single.inc.php'); - } elseif(mb_strtolower(path(0)) === 'login') { - $template = 'login'; - require_once(ROOT.DS.'templates'.DS.'loginform.inc.php'); - - } elseif(mb_strtolower(path(0)) === 'logout') { - $domain = ($_SERVER['HTTP_HOST'] != 'localhost') ? $_SERVER['HTTP_HOST'] : false; - setcookie('microblog_login', '', time()-3600, '/', $domain, false); - unset($_COOKIE['microblog_login']); - - header('Location: '.$config['url']); - die(); - - } elseif(mb_strtolower(path(0)) === 'new') { - $template = 'postform'; - require_once(ROOT.DS.'templates'.DS.'postform.inc.php'); - } else { - // redirect everything else to the homepage - if(!empty(path(0)) && path(0) != 'page') { - header('Location: '.$config['url']); - die(); + $page = mb_strtolower(path(0)); + + switch($page) { + case 'login': + $template = 'login'; + require_once(ROOT.DS.'templates'.DS.'loginform.inc.php'); + break; + case 'logout': + $domain = ($_SERVER['HTTP_HOST'] != 'localhost') ? $_SERVER['HTTP_HOST'] : false; + setcookie('microblog_login', '', time()-3600, '/', $domain, false); + unset($_COOKIE['microblog_login']); + + header('Location: '.$config['url']); + break; + case 'new': + $template = 'postform'; + require_once(ROOT.DS.'templates'.DS.'postform.inc.php'); + break; + case 'rsd': + require_once(ROOT.DS.'lib'.DS.'rsd.xml.php'); + break; + case 'xmlrpc': + require_once(ROOT.DS.'lib'.DS.'xmlrpc.php'); + break; + default: + // redirect everything else to the homepage + if(!empty(path(0)) && path(0) != 'page') { + header('Location: '.$config['url']); + die(); + } + + // show the homepage + require_once(ROOT.DS.'templates'.DS.'timeline.inc.php'); + break; } - - // show the homepage - require_once(ROOT.DS.'templates'.DS.'timeline.inc.php'); } diff --git a/js/microblog.js b/js/microblog.js new file mode 100644 index 0000000..f7963e3 --- /dev/null +++ b/js/microblog.js @@ -0,0 +1,193 @@ +'use strict'; + +document.documentElement.classList.remove('no-js'); + +const textarea = document.querySelector('textarea[name="content"]'); +const charCount = document.querySelector('#count'); + +if (textarea) { + const maxCount = parseInt(textarea.getAttribute('maxlength')); + + if (textarea.value.length > 0) { + const textLength = [...textarea.value].length; + charCount.textContent = maxCount - textLength; + } else { + charCount.textContent = maxCount; + } + + textarea.addEventListener('input', function () { + const textLength = [...this.value].length; + + charCount.textContent = maxCount - textLength; + }, false); +} + +const postForm = document.querySelector('#post-new-form'); +let useDragDrop = (!!window.FileReader && 'draggable' in document.createElement('div')); +useDragDrop = true; +if (postForm) { + const droparea = postForm.querySelector('#post-droparea'); + const attachmentsInput = postForm.querySelector('#post-attachments'); + const data = { + 'attachments': [] + }; + + + if (droparea && attachmentsInput) { + if (useDragDrop) { + console.log('init with modern file attachments'); + + const list = postForm.querySelector('#post-attachments-list'); + list.addEventListener('click', function (e) { + e.preventDefault(); + + // remove attachment + if (e.target.nodeName.toLowerCase() == 'li') { + const filename = e.target.textContent; + + data.attachments = data.attachments.filter(function (ele) { + return ele.name !== filename; + }); + + e.target.remove(); + } + }); + + droparea.classList.remove('hidden'); + document.querySelector('#post-attachments-label').classList.add('hidden'); + + droparea.ondragover = droparea.ondragenter = function (e) { + e.stopPropagation(); + e.preventDefault(); + + e.dataTransfer.dropEffect = 'copy'; + e.target.classList.add('drag'); + }; + + droparea.ondragleave = function (e) { + e.target.classList.remove('drag'); + }; + + droparea.onclick = function (e) { + e.preventDefault(); + + // make a virtual file upload + const input = document.createElement('input'); + input.type = 'file'; + input.setAttribute('multiple', ''); + input.setAttribute('accept', 'image/*'); // only images for now + + input.onchange = e => { + processSelectedFiles(e.target.files); + } + + input.click(); + }; + + function processSelectedFiles(files) { + if (!files || files.length < 1) return; + + for (const file of files) { + const found = data.attachments.find(ele => ele.name === file.name); + if(found) continue; // skip existing attachments + + data.attachments.push({ + 'name': file.name, + // todo: maybe some better form of dupe detection here? + 'file': file + }); + + const li = document.createElement('li'); + li.textContent = file.name; + + const reader = new FileReader(); + + if(file.type.startsWith('image/')) { + reader.onload = function (e) { + var dataURL = e.target.result; + + const preview = document.createElement('img'); + preview.classList.add('file-preview'); + preview.setAttribute('src', dataURL); + + li.prepend(preview); + }; + reader.onerror = function (e) { + console.log('An error occurred during file input: '+e.target.error.code); + }; + + reader.readAsDataURL(file); + } + + list.append(li); + } + } + + droparea.ondrop = function (e) { + if (e.dataTransfer) { + e.preventDefault(); + e.stopPropagation(); + + processSelectedFiles(e.dataTransfer.files); + } + + e.target.classList.remove('drag'); + }; + + postForm.addEventListener('submit', async function (e) { + e.preventDefault(); + + const postFormData = new FormData(); + + postFormData.append('content', postForm.querySelector('[name="content"]').value); + + for (const attachment of data.attachments) { + postFormData.append('attachments[]', attachment.file); + } + + /* + for (const pair of postFormData.entries()) { + console.log(`${pair[0]}, ${pair[1]}`); + } + */ + + const response = await fetch(postForm.getAttribute('action'), { + body: postFormData, + method: 'POST' + }); + + if (response.ok && response.status == 200) { + const txt = await response.text(); + // console.log(response, txt); + window.location.href = postForm.dataset.redirect; + } else { + console.warn('error during post submission!', response); + } + }); + } else { + // use the native file input dialog + // but enhanced + if (attachmentsInput) { + console.log('init with classic file attachments'); + + attachmentsInput.addEventListener('change', function (e) { + console.log(e.target.files); + + const list = postForm.querySelector('#post-attachments-list'); + list.replaceChildren(); + + for (const file of e.target.files) { + const li = document.createElement('li'); + li.textContent = file.name; + list.append(li); + } + }); + } + } + } +} + +// better rounded corners +if ('paintWorklet' in CSS) { + // CSS.paintWorklet.addModule('./js/squircle.js'); +} diff --git a/js/squircle.js b/js/squircle.js new file mode 100644 index 0000000..063a383 --- /dev/null +++ b/js/squircle.js @@ -0,0 +1,146 @@ +const drawSquircle = (ctx, geom, radius, smooth, lineWidth, color) => { + const defaultFill = color; + const lineWidthOffset = lineWidth / 2; + // OPEN LEFT-TOP CORNER + ctx.beginPath(); + ctx.lineTo(radius, lineWidthOffset); + // TOP-RIGHT CORNER + ctx.lineTo(geom.width - radius, lineWidthOffset); + ctx.bezierCurveTo( + geom.width - radius / smooth, + lineWidthOffset, // first bezier point + geom.width - lineWidthOffset, + radius / smooth, // second bezier point + geom.width - lineWidthOffset, + radius // last connect point + ); + // BOTTOM-RIGHT CORNER + ctx.lineTo(geom.width - lineWidthOffset, geom.height - radius); + ctx.bezierCurveTo( + geom.width - lineWidthOffset, + geom.height - radius / smooth, // first bezier point + geom.width - radius / smooth, + geom.height - lineWidthOffset, // second bezier point + geom.width - radius, + geom.height - lineWidthOffset // last connect point + ); + // BOTTOM-LEFT CORNER + ctx.lineTo(radius, geom.height - lineWidthOffset); + ctx.bezierCurveTo( + radius / smooth, + geom.height - lineWidthOffset, // first bezier point + lineWidthOffset, + geom.height - radius / smooth, // second bezier point + lineWidthOffset, + geom.height - radius // last connect point + ); + // CLOSE LEFT-TOP CORNER + ctx.lineTo(lineWidthOffset, radius); + ctx.bezierCurveTo( + lineWidthOffset, + radius / smooth, // first bezier point + radius / smooth, + lineWidthOffset, // second bezier point + radius, + lineWidthOffset // last connect point + ); + ctx.closePath(); + + if (lineWidth) { + // console.log(lineWidth); + ctx.strokeStyle = defaultFill; + ctx.lineWidth = lineWidth; + ctx.stroke(); + } else { + ctx.fillStyle = defaultFill; + ctx.fill(); + } +}; + +if (typeof registerPaint !== "undefined") { + class SquircleClass { + static get contextOptions() { + return { alpha: true }; + } + static get inputProperties() { + return [ + "--squircle-radius", + "--squircle-smooth", + "--squircle-outline", + "--squircle-fill", + "--squircle-ratio", + ]; + } + + paint(ctx, geom, properties) { + const customRatio = properties.get("--squircle-ratio"); + const smoothRatio = 10; + const distanceRatio = parseFloat(customRatio) + ? parseFloat(customRatio) + : 1.8; + const squircleSmooth = parseFloat( + properties.get("--squircle-smooth") * smoothRatio + ); + const squircleRadius = + parseInt(properties.get("--squircle-radius"), 10) * distanceRatio; + const squrcleOutline = parseFloat( + properties.get("--squircle-outline"), + 10 + ); + const squrcleColor = properties + .get("--squircle-fill") + .toString() + .replace(/\s/g, ""); + + const isSmooth = () => { + if (typeof properties.get("--squircle-smooth")[0] !== "undefined") { + if (squircleSmooth === 0) { + return 1; + } + return squircleSmooth; + } else { + return 10; + } + }; + + const isOutline = () => { + if (squrcleOutline) { + return squrcleOutline; + } else { + return 0; + } + }; + + const isColor = () => { + if (squrcleColor) { + return squrcleColor; + } else { + return "#f45"; + } + }; + + if (squircleRadius < geom.width / 2 && squircleRadius < geom.height / 2) { + drawSquircle( + ctx, + geom, + squircleRadius, + isSmooth(), + isOutline(), + isColor() + ); + } else { + drawSquircle( + ctx, + geom, + Math.min(geom.width / 2, geom.height / 2), + isSmooth(), + isOutline(), + isColor() + ); + } + } + } + + // eslint-disable-next-line no-undef + registerPaint("squircle", SquircleClass); +}
\ No newline at end of file diff --git a/lib/database.php b/lib/database.php index 5774d95..4e5e0cd 100644 --- a/lib/database.php +++ b/lib/database.php @@ -15,11 +15,12 @@ try { // first time setup if($config['db_version'] == 0) { try { - $db->exec("CREATE TABLE IF NOT EXISTS `posts` ( - `id` integer PRIMARY KEY NOT NULL, + $db->exec("PRAGMA `user_version` = 1; + CREATE TABLE IF NOT EXISTS `posts` ( + `id` INTEGER PRIMARY KEY NOT NULL, `post_content` TEXT, `post_timestamp` INTEGER - ); PRAGMA `user_version` = 1;"); + );"); $config['db_version'] = 1; } catch(PDOException $e) { print 'Exception : '.$e->getMessage(); @@ -30,7 +31,7 @@ if($config['db_version'] == 0) { // upgrade database to v2 if($config['db_version'] == 1) { try { - $db->exec("PRAGMA user_version = 2; + $db->exec("PRAGMA `user_version` = 2; ALTER TABLE `posts` ADD `post_thread` INTEGER; ALTER TABLE `posts` ADD `post_edited` INTEGER; ALTER TABLE `posts` ADD `post_deleted` INTEGER; @@ -45,7 +46,7 @@ if($config['db_version'] == 1) { // upgrade database to v3 if($config['db_version'] == 2) { try { - $db->exec("PRAGMA user_version = 3; + $db->exec("PRAGMA `user_version` = 3; ALTER TABLE `posts` ADD `post_guid` TEXT; "); $config['db_version'] = 3; @@ -55,5 +56,42 @@ if($config['db_version'] == 2) { } } +// upgrade database to v4 +if($config['db_version'] == 3) { + try { + $db->exec("PRAGMA `user_version` = 4; + CREATE TABLE `files` ( + `id` INTEGER PRIMARY KEY NOT NULL, + `file_filename` TEXT NOT NULL, + `file_extension` TEXT, + `file_original` TEXT NOT NULL, + `file_mime_type` TEXT, + `file_size` INTEGER, + `file_hash` TEXT UNIQUE, + `file_hash_algo` TEXT, + `file_meta` TEXT DEFAULT '{}', + `file_dir` TEXT, + `file_subdir` TEXT, + `file_timestamp` INTEGER, + `file_deleted` INTEGER + ); + CREATE TABLE `file_to_post` ( + `file_id` INTEGER NOT NULL, + `post_id` INTEGER NOT NULL, + `deleted` INTEGER, + UNIQUE(`file_id`, `post_id`) ON CONFLICT IGNORE + ); + CREATE INDEX `posts_timestamp` ON posts (`post_timestamp`); + CREATE INDEX `files_original` ON files (`file_original`); + CREATE INDEX `link_deleted` ON file_to_post (`deleted`); + CREATE UNIQUE INDEX `files_hashes` ON files (`file_hash`); + "); + $config['db_version'] = 4; + } catch(PDOException $e) { + print 'Exception : '.$e->getMessage(); + die('cannot upgrade database table to v4!'); + } +} + // debug: get a list of post table columns // var_dump($db->query("PRAGMA table_info(`posts`)")->fetchAll(PDO::FETCH_COLUMN, 1)); diff --git a/lib/functions.php b/lib/functions.php index 7046eb5..e606230 100644 --- a/lib/functions.php +++ b/lib/functions.php @@ -98,6 +98,33 @@ function db_select_post($id=0) { return (!empty($row)) ? $row : false; } +function db_get_attached_files($post_id, $include_deleted=false) { + global $db; + if(empty($db)) return false; + + $rows = []; + + if($include_deleted) { + $sql = 'SELECT f.* FROM files f LEFT JOIN file_to_post p WHERE f.id = p.file_id AND p.post_id = :post_id ORDER BY f.file_timestamp ASC'; + } else { + $sql = 'SELECT f.* FROM files f LEFT JOIN file_to_post p WHERE f.id = p.file_id AND p.post_id = :post_id AND p.deleted IS NULL ORDER BY f.file_timestamp ASC'; + } + + try { + $statement = $db->prepare($sql); + $statement->bindValue(':post_id', $post_id, PDO::PARAM_INT); + + $statement->execute(); + + $rows = $statement->fetchAll(PDO::FETCH_ASSOC); + } catch(PDOException $e) { + // print 'Exception : '.$e->getMessage(); + return false; + } + + return (!empty($rows)) ? $rows : false; +} + function db_select_posts($from, $amount=10, $sort='desc', $offset=0) { global $db; if(empty($db)) return false; @@ -126,6 +153,225 @@ function db_posts_count() { return (int) $row['posts_count']; } +function convert_files_array($input_array) { + $file_array = []; + $file_count = count($input_array['name']); + $file_keys = array_keys($input_array); + + for ($i=0; $i<$file_count; $i++) { + foreach ($file_keys as $key) { + $file_array[$i][$key] = $input_array[$key][$i]; + } + } + + return $file_array; +} + +function attach_uploaded_files($files=[], $post_id=null) { + if(empty($files['tmp_name'][0])) return false; + + $files = convert_files_array($files); + //var_dump($files);exit(); + + foreach($files as $file) { + + if (!isset($file['error']) || is_array($file['error'])) { + // invalid parameters + // var_dump('bad file info');exit(); + continue; // skip this file + } + + if($file['size'] > 20000000) { + // Exceeded filesize limit. + // var_dump('invalid file size');exit(); + continue; + } + + $mime = mime_content_type($file['tmp_name']); + if (false === $ext = array_search( + $mime, + array( + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'avif' => 'image/avif', + 'webp' => 'image/webp', + // todo: video + 'txt' => 'text/plain', + 'md' => 'text/markdown', + ), + true + )) { + // Invalid file format. + // var_dump('invalid format');exit(); + continue; + } + + save_file($file['name'], $ext, $file['tmp_name'], $post_id, $mime); + } +} + +function detatch_files($file_ids=[], $post_id=null) { + global $db; + if(empty($db)) return false; + if(empty($file_ids)) return false; + if(!$post_id) return false; + + $file_id = null; + + try { + $statement = $db->prepare('UPDATE file_to_post SET deleted = :delete_time WHERE file_id = :file_id AND post_id = :post_id'); + + $statement->bindParam(':file_id', $file_id, PDO::PARAM_INT); + $statement->bindParam(':post_id', $post_id, PDO::PARAM_INT); + $statement->bindValue(':delete_time', time(), PDO::PARAM_INT); + + foreach ($file_ids as $id) { + $file_id = $id; + $statement->execute(); + } + + } catch(PDOException $e) { + // print 'Exception : '.$e->getMessage(); + return false; + } + + return true; +} + +function db_select_file($query, $method='id') { + global $db; + if(empty($db)) return false; + if($id === 0) return false; + + switch ($method) { + case 'hash': + $statement = $db->prepare('SELECT * FROM files WHERE file_hash = :q LIMIT 1'); + $statement->bindValue(':q', $query, PDO::PARAM_STR); + break; + case 'filename': + $statement = $db->prepare('SELECT * FROM files WHERE file_filename = :q LIMIT 1'); + $statement->bindValue(':q', $query, PDO::PARAM_STR); + break; + default: + $statement = $db->prepare('SELECT * FROM files WHERE id = :q LIMIT 1'); + $statement->bindValue(':q', $query, PDO::PARAM_INT); + break; + } + + $statement->execute(); + $row = $statement->fetch(PDO::FETCH_ASSOC); + + return (!empty($row)) ? $row : false; +} + +function db_link_file($file_id, $post_id) { + global $db; + if(empty($db)) return false; + + try { + $statement = $db->prepare('INSERT INTO file_to_post (file_id, post_id) VALUES (:file_id, :post_id)'); + + $statement->bindValue(':file_id', $file_id, PDO::PARAM_INT); + $statement->bindValue(':post_id', $post_id, PDO::PARAM_INT); + + $statement->execute(); + } catch(PDOException $e) { + // print 'Exception : '.$e->getMessage(); + return false; + } + + return true; +} + +function save_file($filename, $extension, $tmp_file, $post_id, $mime='') { + global $db; + if(empty($db)) return false; + + $files_dir = ROOT.DS.'files'; + $hash_algo = 'sha1'; + + $insert = [ + 'file_extension' => $extension, + 'file_original' => $filename, + 'file_mime_type' => $mime, + 'file_size' => filesize($tmp_file), + 'file_hash' => hash_file($hash_algo, $tmp_file), + 'file_hash_algo' => $hash_algo, + 'file_meta' => '{}', + 'file_dir' => date('Y'), + 'file_subdir' => date('m'), + 'file_timestamp' => time() + ]; + + if(!is_dir($files_dir.DS.$insert['file_dir'])) { + mkdir($files_dir.DS.$insert['file_dir'], 0755); + } + + if(!is_dir($files_dir.DS.$insert['file_dir'].DS.$insert['file_subdir'])) { + mkdir($files_dir.DS.$insert['file_dir'].DS.$insert['file_subdir'], 0755); + } + + $insert['file_filename'] = $post_id . '-' . substr($insert['file_hash'], 0, 7); + $path = $files_dir.DS.$insert['file_dir'].DS.$insert['file_subdir']; + + if(move_uploaded_file($tmp_file, $path.DS.$insert['file_filename'] .'.'. $insert['file_extension'])) { + // add to database + + // check if file exists already + $existing = db_select_file($insert['file_hash'], 'hash'); + + if(!empty($existing)) { + // just link existing file + if(db_link_file($existing['id'], $post_id)) { + return $existing['id']; + } else { + return false; + } + } else { + // insert new + try { + $statement = $db->prepare('INSERT INTO files (file_filename, file_extension, file_original, file_mime_type, file_size, file_hash, file_hash_algo, file_meta, file_dir, file_subdir, file_timestamp) VALUES (:file_filename, :file_extension, :file_original, :file_mime_type, :file_size, :file_hash, :file_hash_algo, :file_meta, :file_dir, :file_subdir, :file_timestamp)'); + + $statement->bindValue(':file_filename', $insert['file_filename'], PDO::PARAM_STR); + $statement->bindValue(':file_extension', $insert['file_extension'], PDO::PARAM_STR); + $statement->bindValue(':file_original', $insert['file_original'], PDO::PARAM_STR); + $statement->bindValue(':file_mime_type', $insert['file_mime_type'], PDO::PARAM_STR); + $statement->bindValue(':file_size', $insert['file_size'], PDO::PARAM_INT); + $statement->bindValue(':file_hash', $insert['file_hash'], PDO::PARAM_STR); + $statement->bindValue(':file_hash_algo', $insert['file_hash_algo'], PDO::PARAM_STR); + $statement->bindValue(':file_meta', $insert['file_meta'], PDO::PARAM_STR); + $statement->bindValue(':file_dir', $insert['file_dir'], PDO::PARAM_STR); + $statement->bindValue(':file_subdir', $insert['file_subdir'], PDO::PARAM_STR); + $statement->bindValue(':file_timestamp', $insert['file_timestamp'], PDO::PARAM_INT); + + $statement->execute(); + + // todo: check this? + db_link_file($db->lastInsertId(), $post_id); + + return $db->lastInsertId(); + } catch(PDOException $e) { + print 'Exception : '.$e->getMessage(); + return false; + } + } + } + + return false; +} + +function get_file_path($file) { + $url = ''; + + $url .= 'files/'; + $url .= $file['file_dir'] . '/'; + $url .= $file['file_subdir'] . '/'; + $url .= $file['file_filename'] . '.' . $file['file_extension']; + + return $url; +} + /* function that pings the official micro.blog endpoint for feed refreshes */ function ping_microblog() { global $config; @@ -178,13 +424,33 @@ function rebuild_json_feed($posts=[]) { foreach($posts as $post) { + $attachments = db_get_attached_files($post['id']); + $post_attachments = []; + if(!empty($attachments)) { + foreach ($attachments as $a) { + $post_attachments[] = [ + 'url' => $config['url'] .'/'. get_file_path($a), + 'mime_type' => $a['file_mime_type'], + 'size_in_bytes' => $a['file_size'] + ]; + } + } + + $post_images = array_filter($post_attachments, function($v) { + return strpos($v['mime_type'], 'image') === 0; + }); + $feed['items'][] = array( 'id' => ($post['post_guid'] ? 'urn:uuid:'.$post['post_guid'] : $config['url'].'/'.$post['id']), 'url' => $config['url'].'/'.$post['id'], 'title' => '', 'content_html' => $post['post_content'], - 'date_published' => gmdate('Y-m-d\TH:i:s\Z', $post['post_timestamp']) + 'date_published' => gmdate('Y-m-d\TH:i:s\Z', $post['post_timestamp']), + 'image' => !empty($post_images) ? $post_images[0]['url'] : '', + 'attachments' => $post_attachments ); + + } if(file_put_contents($filename, json_encode($feed, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))) { diff --git a/lib/xmlrpc.php b/lib/xmlrpc.php index 51e1be2..985d0f1 100644 --- a/lib/xmlrpc.php +++ b/lib/xmlrpc.php @@ -3,11 +3,9 @@ $request_xml = file_get_contents("php://input"); // check prerequisites -if(!function_exists('xmlrpc_server_create')) { exit('No XML-RPC support detected!'); } +if(!$config['xmlrpc']) { exit('No XML-RPC support detected!'); } if(empty($request_xml)) { exit('XML-RPC server accepts POST requests only.'); } -// load config -require_once(__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'config.php'); $logfile = ROOT.DS.'log.txt'; if(!function_exists('str_starts_with')) { diff --git a/microblog.js b/microblog.js deleted file mode 100644 index 03e8d11..0000000 --- a/microblog.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const textarea = document.querySelector('textarea[name="content"]'); -const charCount = document.querySelector('#count'); - -if (textarea) { - const maxCount = parseInt(textarea.getAttribute('maxlength')); - - if (textarea.value.length > 0) { - const textLength = [...textarea.value].length; - charCount.textContent = maxCount - textLength; - } else { - charCount.textContent = maxCount; - } - - textarea.addEventListener('input', function () { - const textLength = [...this.value].length; - - charCount.textContent = maxCount - textLength; - }, false); -} diff --git a/snippets/header.snippet.php b/snippets/header.snippet.php index d3270d8..cc0becb 100644 --- a/snippets/header.snippet.php +++ b/snippets/header.snippet.php @@ -9,7 +9,7 @@ header('Content-Type: text/html; charset=utf-8'); ?><!DOCTYPE html> -<html lang="<?= $config['language'] ?>" class="<?= $template ?>"> +<html lang="<?= $config['language'] ?>" class="no-js <?= $template ?>"> <head> <meta charset="utf-8" /> @@ -20,8 +20,15 @@ <link rel="alternate" type="application/json" title="JSON Feed" href="<?= $config['url'] ?>/feed/json" /> <link rel="alternate" type="application/atom+xml" title="Atom Feed" href="<?= $config['url'] ?>/feed/atom" /> <?php if($config['xmlrpc']): ?><link rel="EditURI" type="application/rsd+xml" title="RSD" href="<?= $config['url'] ?>/rsd" /><?php endif; ?> + + <link rel="authorization_endpoint" href="https://micro.blog/indieauth/auth" /> + <link rel="token_endpoint" href="https://micro.blog/indieauth/token" /> + + <?php if(!empty($config['microblog_account'])): ?> + <link href="https://micro.blog/<?= ltrim($config['microblog_account'], '@') ?>" rel="me" /> + <?php endif; ?> <link rel="stylesheet" href="<?= $config['url'] ?>/css/<?= $css ?>.css" /> - <script src="<?= $config['url'] ?>/microblog.js" type="module" defer></script> + <script src="<?= $config['url'] ?>/js/microblog.js" type="module" defer></script> </head> diff --git a/templates/postform.inc.php b/templates/postform.inc.php index 149028b..df7566c 100644 --- a/templates/postform.inc.php +++ b/templates/postform.inc.php @@ -10,7 +10,7 @@ $message = array(); if(!empty($_POST['content'])) { - + $id = db_insert($_POST['content'], NOW); if($id > 0) { @@ -19,6 +19,11 @@ 'message' => 'Successfully posted status #'.$id ); + // handle files + if(!empty($_FILES['attachments'])) { + attach_uploaded_files($_FILES['attachments'], $id); + } + rebuild_feeds(); if($config['ping'] == true) ping_microblog(); if($config['crosspost_to_twitter'] == true) { @@ -43,10 +48,16 @@ <?php if(isset($message['status']) && isset($message['message'])): ?> <p class="message <?= $message['status'] ?>"><?= $message['message'] ?></p> <?php endif; ?> - <form action="" method="post"> + <form action="" method="post" enctype="multipart/form-data" id="post-new-form" data-redirect="<?= $config['url'] ?>"> <textarea name="content" maxlength="<?= $config['max_characters'] ?>"></textarea> - <p id="count"><?= $config['max_characters'] ?></p> - <input type="submit" name="" value="Post" /> + + <div class="post-nav"> + <label id="post-attachments-label">Add Files<input type="file" multiple="multiple" name="attachments[]" id="post-attachments" accept="image/*" /></label> + <div id="post-droparea" class="hidden">Add Files</div> + <ul id="post-attachments-list"></ul> + <p id="count"><?= $config['max_characters'] ?></p> + <input type="submit" name="" value="Post" /> + </div> </form> </div> <?php require(ROOT.DS.'snippets'.DS.'footer.snippet.php'); ?> diff --git a/templates/single.inc.php b/templates/single.inc.php index b858181..64e7f6d 100644 --- a/templates/single.inc.php +++ b/templates/single.inc.php @@ -39,6 +39,32 @@ // edit post if(!empty($_POST['action']) && $_POST['action'] == 'edit') { + // check changes to attachments + $attached_files = db_get_attached_files($_POST['id']); + if(!empty($attached_files)) { + $files_ids = array_column($attached_files, 'id'); + + if(empty($_POST['attachments'])) { + // remove ALL attachments + $to_remove = $files_ids; + } else { + // remove specified attachments + /* + $to_remove = array_filter($attached_files, function($v) { + return !in_array($v['id'], $_POST['attachments']); + }); + */ + $to_remove = array_diff($files_ids, $_POST['attachments']); + } + + if(count($to_remove) > 0) { + if(!detatch_files($to_remove, $_POST['id'])) { + // could not remove attachments + // var_dump($to_remove); + } + } + } + $result = db_update((int) $_POST['id'], $_POST['content']); if(!$result) { @@ -71,12 +97,41 @@ <li class="single-post" data-post-id="<?= $post['id'] ?>"> <?php if($action == 'edit'): ?> <form action="" method="post" class="edit"> - <textarea name="content" maxlength="<?= $config['max_characters'] ?>"><?= $post['post_content'] ?></textarea> - <p id="count"><?= $config['max_characters'] ?></p> - <input type="hidden" name="action" value="edit" /> <input type="hidden" name="id" value="<?= $post['id'] ?>" /> - <input type="submit" class="button" value="Update this post" /> + + <textarea name="content" maxlength="<?= $config['max_characters'] ?>"><?= $post['post_content'] ?></textarea> + + <div class="post-nav"> + <!--<label id="post-attachments-label">Add Files<input type="file" multiple="multiple" name="attachments[]" id="post-attachments" accept="image/*" /></label> + <div id="post-droparea" class="hidden">Add Files</div>--> + <ul id="post-attachments-list"> + <?php + $attachments = db_get_attached_files($post['id']); + ?> + <?php if(!empty($attachments)): ?> + <?php foreach($attachments as $a): ?> + <?php if(strpos($a['file_mime_type'], 'image') === 0): ?> + <?php + $abs = ROOT.DS.get_file_path($a); + list($width, $height, $_, $size_string) = getimagesize($abs); + $url = $config['url'] .'/'. get_file_path($a); + ?> + <li> + <label> + <input type="checkbox" name="attachments[]" value="<?= $a['id'] ?>" checked /> + <img class="file-preview" src="<?= $url ?>" alt="<?= $a['file_original'] ?>" <?= $size_string ?> loading="lazy" /> + <?= $a['file_original'] ?> + </label> + </li> + <?php else: ?> + <?php endif; ?> + <?php endforeach; ?> + <?php endif; ?> + </ul> + <p id="count"><?= $config['max_characters'] ?></p> + <input type="submit" class="button" value="Update this post" /> + </div> </form> <?php else: ?> <?php @@ -85,6 +140,9 @@ $datetime = date_format($date, 'Y-m-d H:i:s'); $formatted_time = date_format($date, 'M d Y H:i'); + + $attachments = db_get_attached_files($post['id']); + // var_dump($attachments); ?> <span class="post-timestamp"> <time class="published" datetime="<?= $datetime ?>" data-unix-time="<?= $post['post_timestamp'] ?>"><?= $formatted_time ?></time> @@ -103,6 +161,29 @@ </ul><?php endif; ?> </nav> <div class="post-content"><?= nl2br(autolink($post['post_content'])) ?></div> + <?php if(!empty($attachments)): ?> + <ul class="post-attachments"> + <?php foreach($attachments as $a): ?> + <li> + <?php if(strpos($a['file_mime_type'], 'image') === 0): ?> + <?php + $abs = ROOT.DS.get_file_path($a); + list($width, $height, $_, $size_string) = getimagesize($abs); + $url = $config['url'] .'/'. get_file_path($a); + ?> + <a href="<?= $url ?>"> + <picture> + <source srcset="<?= $url ?>" type="image/jpeg" /> + <img src="<?= $url ?>" alt="<?= $a['file_original'] ?>" <?= $size_string ?> loading="lazy" /> + </picture> + </a> + <?php else: ?> + <a href="<?= $url ?>" download="<?= $a['file_original'] ?>"><?= $a['file_original'] ?></a> + <?php endif; ?> + </li> + <?php endforeach; ?> + </ul> + <?php endif; ?> <?php if($action == 'delete'): ?> <form action="" method="post" class="delete"> <input type="hidden" name="action" value="delete" /> diff --git a/templates/timeline.inc.php b/templates/timeline.inc.php index bc06de9..6dd37c1 100644 --- a/templates/timeline.inc.php +++ b/templates/timeline.inc.php @@ -36,6 +36,8 @@ $datetime = date_format($date, 'Y-m-d H:i:s'); $formatted_time = date_format($date, 'M d Y H:i'); + + $attachments = db_get_attached_files($post['id']); ?> <a class="post-timestamp" href="<?= $config['url'] ?>/<?= $post['id'] ?>"> <time class="published" datetime="<?= $datetime ?>" data-unix-time="<?= $post['post_timestamp'] ?>"><?= $formatted_time ?></time> @@ -50,6 +52,34 @@ </ul><?php endif; ?> </nav> <div class="post-content"><?= nl2br(autolink($post['post_content'])) ?></div> + <?php if(!empty($attachments)): ?> + <?php + $attachments_total = count($attachments); + // only display the first attachment on the timeline + array_splice($attachments, 1); + ?> + <ul class="post-attachments"> + <?php foreach($attachments as $a): ?> + <li title="<?= ($attachments_total > 1) ? 'and '.($attachments_total-1).' more' : '' ?>"> + <?php if(strpos($a['file_mime_type'], 'image') === 0): ?> + <?php + $abs = ROOT.DS.get_file_path($a); + list($width, $height, $_, $size_string) = getimagesize($abs); + $url = $config['url'] .'/'. get_file_path($a); + ?> + <a href="<?= $config['url'] ?>/<?= $post['id'] ?>"> + <picture> + <source srcset="<?= $url ?>" type="image/jpeg" /> + <img src="<?= $url ?>" alt="<?= $a['file_original'] ?>" <?= $size_string ?> loading="lazy" /> + </picture> + </a> + <?php else: ?> + <a href="<?= $url ?>" download="<?= $a['file_original'] ?>"><?= $a['file_original'] ?></a> + <?php endif; ?> + </li> + <?php endforeach; ?> + </ul> + <?php endif; ?> </li> <?php endforeach; ?> </ul> |