<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/init.inc"; global $title; global $isLoggedIn; global $lang; global $pages; global $_PROFILE; require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/util/random.inc"; $parts = explode("/", $_GET['_']); $select = $parts[2] ?? null; if ($select === "add") { $id = random(); file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs/" . $id . ".json", json_encode([ "name" => "Untitled document ($id)", "category" => null, "contents" => "This is a new document you just created.", "last" => [ "author" => $_PROFILE["login"], "date" => time() ] ])); header("Location: /-/docs/$id"); die(); } elseif (isset($select)) { if (ctype_alnum($select) && file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs/" . $select . ".json")) { $id = $_documentId = $select; $data = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs/" . $select . ".json"), true); $titleBase = " · " . $title . " · Ponycule"; $title = $data["name"] . " · " . $title; } else { header("Location: /-/docs"); die(); } } require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/components/header.inc'; function showDocument($item) { ?> <div style="display: grid; grid-template-columns: 2fr repeat(3, 1fr);"> <span> <?= $item["name"] ?> <?php if (isset($item["nsfw"]) && $item["nsfw"]): ?> <span class="badge bg-danger rounded-pill">NSFW</span> <?php endif; ?> </span> <?php if (str_starts_with(strip_tags($item["contents"]), "/delete")): ?> <span class="badge bg-warning rounded-pill text-black" style="width: max-content;">Deleting in <?= round((($item["last"]["date"] + 86400) - time()) / 3600) ?> hours</span> <?php else: ?> <span class="text-muted"><?= prettySize(filesize($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs/" . $item["id"] . ".json")) ?></span> <span class="relative-time text-muted" data-relative-timestamp="<?= $item["last"]["date"] ?>"><?= timeAgo($item["last"]["date"]) ?></span> <span class="text-muted"><?= $item["last"]["author"] === "raindrops" ? "Raindrops System" : "Cloudburst System" ?></span> <?php endif; ?> </div> <?php } ?> <br> <div class="container"> <div> <?php if (isset($data)): ?><div id="page-content"> <h2> <span contenteditable="true" id="document-name" style="outline: none;"><?= $data["name"] ?></span> <a href="/-/docs" class="small btn btn-outline-light" style="float:right;margin-top:5px;vertical-align:middle;opacity:1 !important; color:white;">Back</a> </h2> <p><b>Category:</b> <span id="category" contenteditable="true" style="outline: none;"><?= $data["category"] ?? "Unsorted" ?></span> · <label style="margin-bottom:5px;"> <input <?= (isset($data["nsfw"]) && $data["nsfw"]) ? "checked" : "" ?> class="form-check-input" type="checkbox" id="explicit"> Explicit </label></p> <p> <?php if ($data["last"]["date"] === 0): ?> Last modified <span id="last-edit-time" class="relative-time" data-relative-timestamp="">never</span> <?php else: ?> Last modified <span id="last-edit-time" class="relative-time" data-relative-timestamp="<?= $data["last"]["date"] ?>"><?= timeAgo($data["last"]["date"]) ?></span> <?php if ($data["last"]["author"] !== $_PROFILE["login"]): ?> <span id="last-edit-author">by <?= $data["last"]["author"] === "raindrops" ? "the Raindrops System" : "the Cloudburst System" ?></span> <?php endif; ?> <?php endif; ?> · <span id="editor-save-status" class="text-muted">Saved</span> </p> <?php $timeDiff = round((1800 - (time() - $data["last"]["date"])) / 60); if ($timeDiff >= 0 && $data["last"]["author"] !== $_PROFILE["login"]): ?> <div class="alert alert-warning"> <b>This document is currently in use by <?= $data["last"]["author"] === "raindrops" ? "the Raindrops System" : "the Cloudburst System" ?>.</b> It has been open in read-only to prevent conflicts with the changes they make. If they stopped editing the document, it will become editable for you after you refresh the page in <?= $timeDiff ?> minute<?= $timeDiff > 1 ? "s" : "" ?>. </div> <?= $data["contents"] ?> <?php else: ?> <textarea id="editor"><?= $data["contents"] ?></textarea> <script src="/assets/editor/editor.js"></script> <script> let editor; ClassicEditor .create( document.querySelector( '#editor' ), { toolbar: [ 'undo', 'redo', '|', 'removeFormat', '|', 'heading', '|', 'fontSize', 'fontColor', 'fontBackgroundColor', 'alignment', '|', 'bold', 'italic', 'underline', 'strikethrough', '|', 'subscript', 'superscript', '|', 'code', '|', 'outdent', 'indent', '|', 'bulletedList', 'numberedList', '|', 'link', 'imageUpload', 'mediaEmbed', 'blockQuote', 'insertTable', 'codeBlock', '|', 'horizontalLine' ] } ) .then( newEditor => { editor = newEditor; } ) .catch( error => { console.error( error ); } ); </script> <!--suppress CssUnresolvedCustomProperty --> <style> :root { --ck-color-base-background: transparent; } .ck-toolbar { filter: invert(1); border-bottom-left-radius: var(--ck-border-radius) !important; border-bottom-right-radius: var(--ck-border-radius) !important; border-bottom-width: 1px !important; } .ck-tooltip__text { color: white !important; } .ck-dropdown__panel { background: #ddd !important; } .ck-color-grid__tile { filter: invert(1); } .ck-balloon-rotator { background-color: #ccc !important; } .ck-balloon-panel { filter: invert(1); } .ck-editor__editable { border-color: transparent !important; } .ck.ck-sticky-panel__content_sticky { top: 59px !important; } </style> <script> let titleBase = "<?= $titleBase ?>"; let lastSavedData = getData(); let lastFetchedData = getData(); let timeSinceLastModified = 0; let saving = false; async function save() { let data = editor.getData(); document.getElementById("editor-save-status").innerHTML = "Saving..."; document.getElementById("editor-save-status").classList.remove("text-danger"); document.getElementById("editor-save-status").classList.remove("text-muted"); document.getElementById("editor-save-status").classList.remove("text-warning"); document.getElementById("editor-save-status").classList.add("text-primary"); saving = true; try { await window.fetch("/api/docs?id=<?= $_documentId ?>", { method: "POST", body: JSON.stringify({ content: data, name: document.getElementById("document-name").innerText, category: document.getElementById("category").innerText, explicit: document.getElementById("explicit").checked }) }); document.getElementById("last-edit-time").setAttribute("data-relative-timestamp", (new Date().getTime() / 1000).toFixed(0)); if (document.getElementById("last-edit-author")) document.getElementById("last-edit-author").outerHTML = ""; document.title = document.getElementById("document-name").innerText + titleBase; document.getElementById("editor-save-status").innerHTML = "Saved"; document.getElementById("editor-save-status").classList.remove("text-danger"); document.getElementById("editor-save-status").classList.add("text-muted"); document.getElementById("editor-save-status").classList.remove("text-warning"); document.getElementById("editor-save-status").classList.remove("text-primary"); lastSavedData = getData(); saving = false; } catch (e) { console.error(e); document.getElementById("editor-save-status").innerHTML = "Failed to save"; document.getElementById("editor-save-status").classList.add("text-danger"); document.getElementById("editor-save-status").classList.remove("text-muted"); document.getElementById("editor-save-status").classList.remove("text-warning"); document.getElementById("editor-save-status").classList.remove("text-primary"); } } document.onclick = async () => { if (saving) return; if (getData() !== lastSavedData) { await save(); } } function getData() { return JSON.stringify({ document: editor.getData(), name: document.getElementById("document-name").innerText, category: document.getElementById("category").innerText, explicit: document.getElementById("explicit").checked }); } setInterval(async () => { if (saving) return; if (getData() !== lastSavedData) { document.getElementById("editor-save-status").innerHTML = "Modified"; document.getElementById("editor-save-status").classList.remove("text-danger"); document.getElementById("editor-save-status").classList.remove("text-muted"); document.getElementById("editor-save-status").classList.add("text-warning"); document.getElementById("editor-save-status").classList.remove("text-primary"); if (getData() !== lastFetchedData) { lastFetchedData = getData(); timeSinceLastModified = 0; } else { timeSinceLastModified++; } if (timeSinceLastModified > 20 || !document.hasFocus()) { await save(); } } else { timeSinceLastModified = 0; } }, 100) </script> <?php endif; ?> </div><?php else: ?> <h2>Documents</h2> <div id="list"> <?php $documents = array_map(function ($i) { return [ "id" => substr($i, 0, -5), ...(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs/" . $i), true) ?? []) ]; }, array_filter(scandir($_SERVER['DOCUMENT_ROOT'] . "/includes/data/docs"), function ($i) { return !str_starts_with($i, ".") && str_ends_with($i, ".json"); })); $deletable = array_values(array_filter($documents, function ($i) { return str_starts_with(strip_tags($i["contents"]), "/delete"); })); $unsorted_pre = array_values(array_filter($documents, function ($i) { return !str_starts_with(strip_tags($i["contents"]), "/delete"); })); $categoryFirst = []; $unsorted = []; $categories = []; foreach ($unsorted_pre as $item) { if (isset($item["category"])) { $existing_categories = [...array_keys($categories), ...array_keys($categoryFirst)]; $matched_category = null; foreach ($existing_categories as $existing_category) { if (levenshtein($item["category"], $existing_category) < 3) { $matched_category = $existing_category; } } $selected_category = $matched_category ?? $item["category"]; if (str_starts_with($item["category"], ".")) { if (!isset($categoryFirst[$selected_category])) $categoryFirst[$selected_category] = []; $categoryFirst[$selected_category][] = $item; } else { if (!isset($categories[$selected_category])) $categories[$selected_category] = []; $categories[$selected_category][] = $item; } } else { $unsorted[] = $item; } } foreach ($categories as $category => $_) { uasort($categories[$category], function ($a, $b) { return preg_replace("/[^a-z\d]/m", "", strtolower($b["name"])) < preg_replace("/[^a-z\d]/m", "", strtolower($a["name"])); }); } foreach ($categoryFirst as $category => $_) { uasort($categoryFirst[$category], function ($a, $b) { return preg_replace("/[^a-z\d]/m", "", strtolower($b["name"])) < preg_replace("/[^a-z\d]/m", "", strtolower($a["name"])); }); } uasort($unsorted, function ($a, $b) { return preg_replace("/[^a-z\d]/m", "", strtolower($b["name"])) < preg_replace("/[^a-z\d]/m", "", strtolower($a["name"])); }); uasort($deletable, function ($a, $b) { return preg_replace("/[^a-z\d]/m", "", strtolower($b["name"])) < preg_replace("/[^a-z\d]/m", "", strtolower($a["name"])); }); $fullList = [...$categoryFirst, ...$categories]; ?> <?php foreach ($fullList as $category => $items): if ($category != "Archives"): ?> <h4><?= str_starts_with($category, ".") ? substr($category, 1) : $category ?></h4><div class="list-group"> <?php foreach ($items as $item): ?> <a href="/-/docs/<?= $item["id"] ?>" id="document-<?= $item["id"] ?>" class="list-group-item list-group-item-action document-listing <?php if (str_starts_with(strip_tags($item["contents"]), "/delete")): ?>opacity-75<?php endif; ?>"> <?php showDocument($item) ?> </a> <?php endforeach; ?></div> <hr> <?php endif; endforeach; ?> <h4>Unsorted</h4><div class="list-group"> <?php foreach ($unsorted as $item): ?> <a href="/-/docs/<?= $item["id"] ?>" id="document-<?= $item["id"] ?>" class="list-group-item list-group-item-action document-listing <?php if (str_starts_with(strip_tags($item["contents"]), "/delete")): ?>opacity-75<?php endif; ?>"> <?php showDocument($item) ?> </a> <?php endforeach; ?></div> <hr> <details> <summary>Show archives and marked for deletion</summary> <h4 style="margin-top: 10px;">Archives</h4> <div class="list-group"> <?php foreach ($categories["Archives"] as $item): ?> <a href="/-/docs/<?= $item["id"] ?>" id="document-<?= $item["id"] ?>" class="list-group-item list-group-item-action document-listing <?php if (str_starts_with(strip_tags($item["contents"]), "/delete")): ?>opacity-75<?php endif; ?>"> <?php showDocument($item) ?> </a> <?php endforeach; ?></div> <hr> <h4 id="deletable">Marked for deletion</h4><div class="list-group"> <?php foreach ($deletable as $item): ?> <a href="/-/docs/<?= $item["id"] ?>" id="document-<?= $item["id"] ?>" class="list-group-item list-group-item-action document-listing <?php if (str_starts_with(strip_tags($item["contents"]), "/delete")): ?>opacity-75<?php endif; ?>"> <?php showDocument($item) ?> </a> <?php endforeach; ?></div> </details> </div> <hr> <div id="page-content"> <a href="/-/docs/add">Create a new document</a> </div> <?php endif; ?> </div> </div> <script> setInterval(async () => { Array.from(document.getElementsByClassName("relative-time")).forEach((el) => { el.innerText = timeAgo(parseInt(el.getAttribute("data-relative-timestamp")) * 1000); }) }, 1000) function timeAgo(time) { if (!isNaN(parseInt(time))) { time = new Date(time).getTime(); } let periods = ["sec", "min", "hr", "d", "wk", "mo", "y", "ages"]; let lengths = ["60", "60", "24", "7", "4.35", "12", "100"]; let now = new Date().getTime(); let difference = Math.round((now - time) / 1000); let tense; let period; if (difference <= 10 && difference >= 0) { return "now"; } else if (difference > 0) { tense = "ago"; } else { tense = "later"; } let j; for (j = 0; difference >= lengths[j] && j < lengths.length - 1; j++) { difference /= lengths[j]; } difference = Math.round(difference); period = periods[j]; return `${difference} ${period} ${tense}`; } </script> <style> <?php global $use2023UI; if (!$use2023UI): ?> .list-group-item { color: #fff; background-color: #222; border: 1px solid rgba(255, 255, 255, .125); } .list-group-item.disabled { color: #fff; background-color: #222; border-color: rgba(255, 255, 255, .125); opacity: .75; } .list-group-item:hover { background-color: #252525; color: #ddd; } .list-group-item:active, .list-group-item:focus { background-color: #272727; color: #bbb; } <?php endif; ?> </style> <?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/components/footer.inc'; ?>