Fixes
This commit is contained in:
7 files changed
+119
-106
No files matched your search
@@ -24,11 +24,11 @@ Route handlers can access the shared database pool with the `State(pool): State<
|
|||||||
|
|
||||||
## Browser app
|
## Browser app
|
||||||
|
|
||||||
Open `/gui` for the browser workspace. Create text pastes, one-time secrets, and short links; choose custom IDs, languages, expiry, and view limits; and open shares with syntax highlighting or as plain text.
|
Open `/gui` for the browser workspace. Create text pastes, one-time secrets, and short links; choose custom IDs, languages, expiry, and view limits. After saving, a dialog prominently displays the returned link for copying or opening. Opening a share navigates directly to its page or redirect.
|
||||||
|
|
||||||
The language dropdown is populated from irosashi's supported grammars. The home page renders this README as HTML and shares the browser app's logo, navigation, and styling.
|
The language dropdown is populated from irosashi's supported grammars. The home page renders this README as HTML and shares the browser app's logo, navigation, and styling.
|
||||||
|
|
||||||
Sign in with HTTP Basic credentials to browse, filter, edit, and delete your own shares. A new username creates an account. Credentials stay in page memory and are cleared on sign-out or reload. The app uses the existing API without page reloads.
|
Sign in with HTTP Basic credentials to browse, filter, edit, and delete your own shares. A new username creates an account. Credentials stay in page memory and are cleared on sign-out or reload. Creating and managing shares uses the existing API without page reloads.
|
||||||
|
|
||||||
Opening a paste or following a short link counts as a view. Listing records and loading them into the editor do not. Updates preserve omitted options and view counters.
|
Opening a paste or following a short link counts as a view. Listing records and loading them into the editor do not. Updates preserve omitted options and view counters.
|
||||||
|
|
||||||
@@ -104,7 +104,9 @@ curl --request POST 'http://localhost:3000/once/my-secret' \
|
|||||||
|
|
||||||
**Returns:** the paste as HTML or plain text, based on the `Accept` header. The default is plain text. HTML uses the stored language for syntax highlighting, falling back to plain text for missing or unknown languages.
|
**Returns:** the paste as HTML or plain text, based on the `Accept` header. The default is plain text. HTML uses the stored language for syntax highlighting, falling back to plain text for missing or unknown languages.
|
||||||
|
|
||||||
HTML and GUI previews show the view counter, including the current read, and the maximum when set. Both representations include `X-Paste-Views` and, when limited, `X-Paste-Max-Views` response headers, so displaying the counter does not require another read.
|
HTML paste pages show the view counter, including the current read, and the maximum when set. HTML and plain-text responses include `X-Paste-Views` and, when limited, `X-Paste-Max-Views` response headers, so displaying the counter does not require another read.
|
||||||
|
|
||||||
|
The HTML page includes a **Copy raw code** button that preserves the original content and whitespace without another request or view. If clipboard access is unavailable, the raw text is selected for manual copying.
|
||||||
|
|
||||||
Each successful read increments `views`. The final permitted read returns the content and deletes the paste. Missing, expired, and exhausted pastes return `404`. Expired and exhausted records are also cleaned up in the background every minute.
|
Each successful read increments `views`. The final permitted read returns the content and deletes the paste. Missing, expired, and exhausted pastes return `404`. Expired and exhausted records are also cleaned up in the background every minute.
|
||||||
|
|
||||||
|
|||||||
+14
-3
@@ -5,6 +5,7 @@ use axum::{
|
|||||||
http::{HeaderMap, HeaderValue, StatusCode},
|
http::{HeaderMap, HeaderValue, StatusCode},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||||
use irosashi::{CodeToHtmlOptions, Highlighter};
|
use irosashi::{CodeToHtmlOptions, Highlighter};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ use super::{db_error, return_html};
|
|||||||
pub struct NotePage {
|
pub struct NotePage {
|
||||||
paste: Paste,
|
paste: Paste,
|
||||||
code: String,
|
code: String,
|
||||||
|
raw_code: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_note(
|
pub async fn get_note(
|
||||||
@@ -24,10 +26,12 @@ pub async fn get_note(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Response, StatusCode> {
|
) -> Result<Response, StatusCode> {
|
||||||
let paste = Paste::read(&db, &id, PasteType::TextV1)
|
let Some(paste) = Paste::read(&db, &id, PasteType::TextV1)
|
||||||
.await
|
.await
|
||||||
.map_err(db_error)?
|
.map_err(db_error)?
|
||||||
.ok_or(StatusCode::NOT_FOUND)?;
|
else {
|
||||||
|
return Ok((StatusCode::NOT_FOUND, "Note not found").into_response());
|
||||||
|
};
|
||||||
|
|
||||||
let mut metadata = HeaderMap::new();
|
let mut metadata = HeaderMap::new();
|
||||||
metadata.insert("x-paste-views", HeaderValue::from(paste.views));
|
metadata.insert("x-paste-views", HeaderValue::from(paste.views));
|
||||||
@@ -46,9 +50,16 @@ pub async fn get_note(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
NotePage { paste, code }.into_response()
|
let raw_code = STANDARD.encode(&paste.content);
|
||||||
|
NotePage {
|
||||||
|
paste,
|
||||||
|
code,
|
||||||
|
raw_code,
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
} else {
|
} else {
|
||||||
paste.content.into_response()
|
paste.content.into_response()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((metadata, response).into_response())
|
Ok((metadata, response).into_response())
|
||||||
}
|
}
|
||||||
+27
-11
@@ -310,15 +310,6 @@ button:hover {
|
|||||||
color: #923e31;
|
color: #923e31;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-button {
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--accent);
|
|
||||||
text-decoration: underline;
|
|
||||||
text-align: left;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
@@ -376,8 +367,33 @@ hr {
|
|||||||
color: #7f3022;
|
color: #7f3022;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result, .preview {
|
dialog.result {
|
||||||
margin-top: 1.25rem;
|
box-sizing: border-box;
|
||||||
|
width: min(36rem, calc(100% - 2rem));
|
||||||
|
max-height: calc(100vh - 2rem);
|
||||||
|
box-shadow: 0 1.5rem 5rem rgb(20 35 20 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog::backdrop {
|
||||||
|
background: rgb(20 35 20 / 45%);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result #share-url {
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-block: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result [data-error="true"] {
|
||||||
|
color: #7f3022;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result p:last-child {
|
.result p:last-child {
|
||||||
|
|||||||
+28
-74
@@ -6,12 +6,13 @@ let notes = [];
|
|||||||
let editing = null;
|
let editing = null;
|
||||||
|
|
||||||
function notify(message, error = false) {
|
function notify(message, error = false) {
|
||||||
$("status").textContent = message;
|
const target = $("share-result").open ? $("result-feedback") : $("status");
|
||||||
$("status").dataset.error = String(error);
|
target.textContent = message;
|
||||||
$("status").hidden = false;
|
target.dataset.error = String(error);
|
||||||
|
target.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function api(path, { method = "GET", body, accept = "text/plain", includeHeaders = false } = {}, auth = authorization) {
|
async function api(path, { method = "GET", body, accept = "text/plain" } = {}, auth = authorization) {
|
||||||
const headers = { Accept: accept };
|
const headers = { Accept: accept };
|
||||||
if (auth) headers.Authorization = auth;
|
if (auth) headers.Authorization = auth;
|
||||||
const response = await fetch(path, { method, body, headers, credentials: "omit", cache: "no-store" });
|
const response = await fetch(path, { method, body, headers, credentials: "omit", cache: "no-store" });
|
||||||
@@ -26,8 +27,7 @@ async function api(path, { method = "GET", body, accept = "text/plain", includeH
|
|||||||
};
|
};
|
||||||
throw new Error(messages[response.status] || `The request failed (${response.status}). Please try again.`);
|
throw new Error(messages[response.status] || `The request failed (${response.status}). Please try again.`);
|
||||||
}
|
}
|
||||||
const data = accept === "application/json" ? await response.json() : await response.text();
|
return accept === "application/json" ? response.json() : response.text();
|
||||||
return includeHeaders ? { data, headers: response.headers } : data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function busy(button, action) {
|
async function busy(button, action) {
|
||||||
@@ -96,7 +96,10 @@ function renderNotes() {
|
|||||||
for (const note of filtered) {
|
for (const note of filtered) {
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
const name = cell(row, "");
|
const name = cell(row, "");
|
||||||
name.append(action(note.id, () => openEntry(note), "link-button"));
|
const link = document.createElement("a");
|
||||||
|
link.href = sharePath(note.id, isUrl(note));
|
||||||
|
link.textContent = note.id;
|
||||||
|
name.append(link);
|
||||||
const type = document.createElement("small");
|
const type = document.createElement("small");
|
||||||
type.textContent = isUrl(note) ? "Short link" : note.language || "Plain text";
|
type.textContent = isUrl(note) ? "Short link" : note.language || "Plain text";
|
||||||
name.append(type);
|
name.append(type);
|
||||||
@@ -154,7 +157,7 @@ function resetEditor() {
|
|||||||
editing = null;
|
editing = null;
|
||||||
$("language").querySelector("[data-current]")?.remove();
|
$("language").querySelector("[data-current]")?.remove();
|
||||||
$("editor-form").reset();
|
$("editor-form").reset();
|
||||||
$("share-result").hidden = true;
|
$("share-result").close();
|
||||||
updateEditor();
|
updateEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,53 +202,6 @@ function reference(value, url) {
|
|||||||
return { id, url };
|
return { id, url };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openPaste(id) {
|
|
||||||
$("preview").replaceChildren();
|
|
||||||
$("preview").hidden = true;
|
|
||||||
const highlighted = $("open-format").value === "html";
|
|
||||||
const { data: content, headers } = await api(sharePath(id, false), {
|
|
||||||
accept: highlighted ? "text/html" : "text/plain",
|
|
||||||
includeHeaders: true,
|
|
||||||
});
|
|
||||||
const counter = document.createElement("p");
|
|
||||||
counter.className = "badge view-counter";
|
|
||||||
counter.textContent = `Views: ${headers.get("x-paste-views")} / ${headers.get("x-paste-max-views") ?? "∞"}`;
|
|
||||||
$("preview").append(counter);
|
|
||||||
if (highlighted) {
|
|
||||||
const page = new DOMParser().parseFromString(content, "text/html");
|
|
||||||
const code = page.querySelector("main pre");
|
|
||||||
if (!code) throw new Error("The server returned an unexpected preview.");
|
|
||||||
$("preview").append(document.importNode(code, true));
|
|
||||||
} else {
|
|
||||||
const pre = document.createElement("pre");
|
|
||||||
pre.textContent = content;
|
|
||||||
$("preview").append(pre);
|
|
||||||
}
|
|
||||||
$("preview").hidden = false;
|
|
||||||
await loadNotes();
|
|
||||||
notify(`Opened ${id}. This read counts as a view.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openEntry(note) {
|
|
||||||
$("open-id").value = note.id;
|
|
||||||
$("open-kind").value = isUrl(note) ? "url" : "note";
|
|
||||||
updateOpen();
|
|
||||||
showPanel("open");
|
|
||||||
if (isUrl(note)) {
|
|
||||||
window.open(sharePath(note.id, true), "_blank", "noopener,noreferrer");
|
|
||||||
notify("The short link opens in a new tab. Refresh your collection to update its view count.");
|
|
||||||
} else {
|
|
||||||
await openPaste(note.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateOpen() {
|
|
||||||
const url = $("open-kind").value === "url";
|
|
||||||
$("format-field").hidden = url;
|
|
||||||
$("open-submit").textContent = url ? "Follow short link ↗" : "Open paste";
|
|
||||||
$("preview").hidden = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$("login-form").addEventListener("submit", event => {
|
$("login-form").addEventListener("submit", event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
busy(event.submitter, async () => {
|
busy(event.submitter, async () => {
|
||||||
@@ -272,8 +228,6 @@ $("logout").addEventListener("click", () => {
|
|||||||
$("login-form").reset();
|
$("login-form").reset();
|
||||||
$("logout").hidden = true;
|
$("logout").hidden = true;
|
||||||
$("session-label").textContent = "Anonymous";
|
$("session-label").textContent = "Anonymous";
|
||||||
$("preview").replaceChildren();
|
|
||||||
$("preview").hidden = true;
|
|
||||||
resetEditor();
|
resetEditor();
|
||||||
renderNotes();
|
renderNotes();
|
||||||
notify("Signed out.");
|
notify("Signed out.");
|
||||||
@@ -282,6 +236,7 @@ $("logout").addEventListener("click", () => {
|
|||||||
$("editor-form").addEventListener("submit", event => {
|
$("editor-form").addEventListener("submit", event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
busy(event.submitter, async () => {
|
busy(event.submitter, async () => {
|
||||||
|
const current = editing;
|
||||||
const url = $("kind").value === "url";
|
const url = $("kind").value === "url";
|
||||||
const body = new URLSearchParams();
|
const body = new URLSearchParams();
|
||||||
body.set(url ? "href" : "content", $("content").value);
|
body.set(url ? "href" : "content", $("content").value);
|
||||||
@@ -290,22 +245,27 @@ $("editor-form").addEventListener("submit", event => {
|
|||||||
if ($("max-views").value !== "") body.set("max_views", $("max-views").value);
|
if ($("max-views").value !== "") body.set("max_views", $("max-views").value);
|
||||||
const requested = $("custom-id").value.trim();
|
const requested = $("custom-id").value.trim();
|
||||||
let path = url ? "/to" : $("once").checked ? "/once" : "";
|
let path = url ? "/to" : $("once").checked ? "/once" : "";
|
||||||
if (editing) path = sharePath(editing.id, url);
|
if (current) path = sharePath(current.id, url);
|
||||||
else path = requested ? `${path}/${encodeURIComponent(requested)}` : path || "/";
|
else path = requested ? `${path}/${encodeURIComponent(requested)}` : path || "/";
|
||||||
const result = await api(path, { method: editing ? "PUT" : "POST", body });
|
const result = await api(path, { method: current ? "PUT" : "POST", body });
|
||||||
const id = editing ? editing.id : result;
|
const id = current ? current.id : result;
|
||||||
const link = new URL(sharePath(id, url), location.origin).href;
|
const link = new URL(sharePath(id, url), location.origin).href;
|
||||||
$("share-url").value = link;
|
$("share-url").value = link;
|
||||||
$("share-link").href = link;
|
$("share-link").href = link;
|
||||||
$("result-title").textContent = editing ? "Changes saved" : "Your share is ready";
|
$("result-title").textContent = current ? "Changes saved" : "Your share is ready";
|
||||||
$("share-result").hidden = false;
|
const message = `${current ? "Updated" : "Created"} ${id}.${requested && requested !== id ? " The requested ID was unavailable, so a new one was generated." : ""}`;
|
||||||
|
$("result-feedback").textContent = message;
|
||||||
|
$("result-feedback").dataset.error = "false";
|
||||||
|
$("result-feedback").hidden = false;
|
||||||
|
$("share-result").showModal();
|
||||||
|
$("share-url").focus();
|
||||||
|
$("share-url").select();
|
||||||
await loadNotes();
|
await loadNotes();
|
||||||
if (editing) {
|
if (current && editing === current) {
|
||||||
editing = notes.find(note => note.id === id) || editing;
|
editing = notes.find(note => note.id === id) || editing;
|
||||||
$("expires").value = "";
|
$("expires").value = "";
|
||||||
updateEditor();
|
updateEditor();
|
||||||
}
|
}
|
||||||
notify(`${editing ? "Updated" : "Created"} ${id}.${requested && requested !== id ? " The requested ID was unavailable, so a new one was generated." : ""}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -322,17 +282,12 @@ $("copy-url").addEventListener("click", event => busy(event.currentTarget, async
|
|||||||
|
|
||||||
$("open-form").addEventListener("submit", event => {
|
$("open-form").addEventListener("submit", event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
busy(event.submitter, async () => {
|
try {
|
||||||
const ref = reference($("open-id").value, $("open-kind").value === "url");
|
const ref = reference($("open-id").value, $("open-kind").value === "url");
|
||||||
$("open-kind").value = ref.url ? "url" : "note";
|
location.assign(sharePath(ref.id, ref.url));
|
||||||
updateOpen();
|
} catch (error) {
|
||||||
if (ref.url) {
|
notify(error.message, true);
|
||||||
window.open(sharePath(ref.id, true), "_blank", "noopener,noreferrer");
|
|
||||||
notify("The short link opens in a new tab. Following it counts as a view.");
|
|
||||||
} else {
|
|
||||||
await openPaste(ref.id);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const link of document.querySelectorAll("[data-panel]")) {
|
for (const link of document.querySelectorAll("[data-panel]")) {
|
||||||
@@ -344,7 +299,6 @@ for (const link of document.querySelectorAll("[data-panel]")) {
|
|||||||
$("kind").addEventListener("change", updateEditor);
|
$("kind").addEventListener("change", updateEditor);
|
||||||
$("once").addEventListener("change", updateEditor);
|
$("once").addEventListener("change", updateEditor);
|
||||||
$("new-note").addEventListener("click", resetEditor);
|
$("new-note").addEventListener("click", resetEditor);
|
||||||
$("open-kind").addEventListener("change", updateOpen);
|
|
||||||
$("search").addEventListener("input", renderNotes);
|
$("search").addEventListener("input", renderNotes);
|
||||||
$("refresh").addEventListener("click", event => busy(event.currentTarget, loadNotes));
|
$("refresh").addEventListener("click", event => busy(event.currentTarget, loadNotes));
|
||||||
window.addEventListener("hashchange", () => showPanel(location.hash.slice(1)));
|
window.addEventListener("hashchange", () => showPanel(location.hash.slice(1)));
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const copy = document.getElementById("copy-code");
|
||||||
|
const status = document.getElementById("copy-status");
|
||||||
|
const fallback = document.getElementById("raw-code");
|
||||||
|
|
||||||
|
copy.addEventListener("click", async () => {
|
||||||
|
const bytes = Uint8Array.from(atob(copy.dataset.raw), character => character.charCodeAt(0));
|
||||||
|
const content = new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes);
|
||||||
|
copy.disabled = true;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
fallback.hidden = true;
|
||||||
|
} catch {
|
||||||
|
fallback.value = content;
|
||||||
|
fallback.hidden = false;
|
||||||
|
fallback.focus();
|
||||||
|
fallback.select();
|
||||||
|
} finally {
|
||||||
|
copy.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
+17
-15
@@ -89,15 +89,6 @@
|
|||||||
<p class="hint">If a custom ID is taken, you'll get a fresh one. Always use the link returned after saving.</p>
|
<p class="hint">If a custom ID is taken, you'll get a fresh one. Always use the link returned after saving.</p>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<section id="share-result" class="card result" aria-labelledby="result-title" hidden>
|
|
||||||
<p class="eyebrow">READY TO SHARE</p>
|
|
||||||
<h2 id="result-title">Your link</h2>
|
|
||||||
<div class="copy-row">
|
|
||||||
<input id="share-url" aria-label="Share URL" readonly>
|
|
||||||
<button id="copy-url" type="button" class="secondary">Copy link</button>
|
|
||||||
</div>
|
|
||||||
<p><a id="share-link" target="_blank" rel="noopener noreferrer">Open share ↗</a> <span class="hint">Opening counts as a view.</span></p>
|
|
||||||
</section>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="collection" class="panel card" aria-labelledby="collection-title" hidden>
|
<section id="collection" class="panel card" aria-labelledby="collection-title" hidden>
|
||||||
@@ -120,15 +111,26 @@
|
|||||||
<p class="muted">Have an ID or a Pastable link? Open it here.</p>
|
<p class="muted">Have an ID or a Pastable link? Open it here.</p>
|
||||||
<form id="open-form">
|
<form id="open-form">
|
||||||
<label>ID or share URL<input id="open-id" placeholder="my-paste or a full share URL" required autocomplete="off"></label>
|
<label>ID or share URL<input id="open-id" placeholder="my-paste or a full share URL" required autocomplete="off"></label>
|
||||||
<div class="form-grid">
|
|
||||||
<label>Type<select id="open-kind"><option value="note">Text or code</option><option value="url">Short link</option></select></label>
|
<label>Type<select id="open-kind"><option value="note">Text or code</option><option value="url">Short link</option></select></label>
|
||||||
<label id="format-field">Display<select id="open-format"><option value="html">Syntax highlighted</option><option value="plain">Plain text</option></select></label>
|
<button type="submit">Open share</button>
|
||||||
</div>
|
<p class="hint">Opening a share navigates to it and counts as a view.</p>
|
||||||
<button type="submit" id="open-submit">Open paste</button>
|
|
||||||
<p class="hint">Opening a paste or following a link counts as a view. Short links open in a new tab.</p>
|
|
||||||
</form>
|
</form>
|
||||||
<div id="preview" class="preview" aria-live="polite" hidden></div>
|
|
||||||
</section>
|
</section>
|
||||||
|
<dialog id="share-result" class="card result" aria-labelledby="result-title" aria-describedby="result-feedback">
|
||||||
|
<p class="eyebrow">READY TO SHARE</p>
|
||||||
|
<h2 id="result-title">Your share is ready</h2>
|
||||||
|
<p id="result-feedback" class="hint" role="status" aria-live="polite"></p>
|
||||||
|
<label for="share-url">Share link</label>
|
||||||
|
<div class="copy-row">
|
||||||
|
<input id="share-url" readonly autofocus>
|
||||||
|
<button id="copy-url" type="button">Copy link</button>
|
||||||
|
</div>
|
||||||
|
<div class="actions result-actions">
|
||||||
|
<a id="share-link">Open share ↗</a>
|
||||||
|
<form method="dialog"><button class="secondary">Done</button></form>
|
||||||
|
</div>
|
||||||
|
<p class="hint">Opening counts as a view. Copying the link does not.</p>
|
||||||
|
</dialog>
|
||||||
<noscript><p class="status">Enable JavaScript to use the browser app. The HTTP API remains available.</p></noscript>
|
<noscript><p class="status">Enable JavaScript to use the browser app. The HTTP API remains available.</p></noscript>
|
||||||
<script src="/static/gui.js" defer></script>
|
<script src="/static/gui.js" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,8 +4,14 @@
|
|||||||
<article class="card paste-view">
|
<article class="card paste-view">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div><p class="eyebrow">SHARED PASTE</p><h1>{{ paste.id }}</h1></div>
|
<div><p class="eyebrow">SHARED PASTE</p><h1>{{ paste.id }}</h1></div>
|
||||||
|
<div class="actions">
|
||||||
<p class="badge view-counter">Views: {{ paste.views }} / {% match paste.max_views %}{% when Some with (max) %}{{ max }}{% when None %}∞{% endmatch %}</p>
|
<p class="badge view-counter">Views: {{ paste.views }} / {% match paste.max_views %}{% when Some with (max) %}{{ max }}{% when None %}∞{% endmatch %}</p>
|
||||||
|
<button id="copy-code" class="secondary" type="button" data-raw="{{ raw_code }}">Copy raw code</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ code|safe }}
|
{{ code|safe }}
|
||||||
|
<p id="copy-status" class="hint" role="status" aria-live="polite" hidden></p>
|
||||||
|
<textarea id="raw-code" aria-label="Raw paste content" rows="8" readonly spellcheck="false" hidden></textarea>
|
||||||
</article>
|
</article>
|
||||||
|
<script src="/static/paste.js" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in new issue
Block a user