Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ee3ffbfc9 | ||
|
|
53e7a97c44 | ||
|
|
e7ecb9789b | ||
|
|
3c5ccfb787 | ||
|
|
1f745b6e59 | ||
|
|
a9f94ce806 | ||
|
|
785fafcaf9 |
@@ -1,7 +1,7 @@
|
||||
# Palette
|
||||
|
||||
Palette is a fast, self-hosted pastebin. One Go binary, a SQLite database, and
|
||||
a web UI for sharing text and small files
|
||||
a web UI for sharing text and small files.
|
||||
|
||||
> [!NOTE]
|
||||
> <table><tr><td>
|
||||
@@ -10,22 +10,27 @@ a web UI for sharing text and small files
|
||||
|
||||
## Features
|
||||
|
||||
- Multiple files in one paste
|
||||
- Text pastes and cans (multiple items in one share)
|
||||
- File attachments (one file per paste, up to 25 MB)
|
||||
- Password protected pastes
|
||||
- Expir after a specified time
|
||||
- Expire after a specified time
|
||||
- Burn after a number of views
|
||||
- Custom URLs
|
||||
- Syntax highlighting with language auto-detection (go-enry)
|
||||
- Local cookie based submission history
|
||||
- Cookie based settings
|
||||
- Themes!
|
||||
- Public paste listing with search, sort and pagination
|
||||
- Cookie based saved pastes and settings
|
||||
- Five base themes (midnight, smooth, pastel-lavender, pastel-peach, pastel-cloud), each with a dark and light variant
|
||||
- Dark mode toggle in the topbar and settings, with a configurable default
|
||||
|
||||
## Screenshots
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
Mobile previews (375x812): [paste view](https://git.archfox.org/poslop/palette/wiki/raw/palette-previews%2Fmobile-paste-midnight-dark.png), [public list](https://git.archfox.org/poslop/palette/wiki/raw/palette-previews%2Fmobile-public.png), [settings](https://git.archfox.org/poslop/palette/wiki/raw/palette-previews%2Fmobile-settings.png).
|
||||
|
||||
## Get Started
|
||||
|
||||
@@ -64,9 +69,10 @@ go build -o palette ./cmd/palette
|
||||
| `PALETTE_MAX_ITEM` | `26214400` | Max can item size in bytes (25 MB) |
|
||||
| `PALETTE_ADMIN_KEY` | generated | Admin key; if unset a 32-char hex key is generated and persisted to `<db-dir>/admin-key` (0600) |
|
||||
| `PALETTE_DEFAULT_DARK` | dark on | Default dark mode for new visitors. Set `false`, `0`, or `off` to default to light mode. Visitors who toggle dark mode keep their choice in their browser. |
|
||||
| `PALETTE_UNLOCK_SECRET` | random per start | HMAC secret for password-unlock cookies. Set a fixed value to keep unlock sessions across restarts or across replicas. |
|
||||
|
||||
An `/admin` page exists for runtime settings, protected by a key set at
|
||||
install (`PALETTE_ADMIN_KEY` env var) and resettable locally — see
|
||||
install (`PALETTE_ADMIN_KEY` env var) and resettable locally. See
|
||||
[API](https://git.archfox.org/poslop/palette/wiki/API) and the [design docs](https://git.archfox.org/poslop/palette/wiki/Home) in the wiki for details.
|
||||
|
||||
## API
|
||||
|
||||
+11
-13
@@ -307,21 +307,19 @@ function setAttachedFile(file) {
|
||||
attachedFile = file;
|
||||
renderFileChip();
|
||||
setHidden('file-text-note', false);
|
||||
// #171/#184: title auto-fill — images take the file name; text files
|
||||
// use the language-placeholder convention (e.g. Python.py, fallback
|
||||
// Text.txt). Only when the title is still blank; never overwrite a
|
||||
// typed title.
|
||||
// #233: title auto-fill — the file's own name always wins when the
|
||||
// title is still blank; fallback is date.fileextension (e.g.
|
||||
// 2026-09-10.txt) when the name is missing or unusable. Never
|
||||
// overwrite a typed title.
|
||||
if (!$('title').value.trim()) {
|
||||
if (IMAGE_RE.test(file.type)) {
|
||||
$('title').value = file.name;
|
||||
const raw = (file.name || '').trim();
|
||||
if (raw) {
|
||||
$('title').value = raw;
|
||||
} else {
|
||||
const lang = LANG_BY_EXT[extOf(file.name)];
|
||||
if (lang) {
|
||||
const fn = defaultFilename(lang);
|
||||
if (fn) $('title').value = fn;
|
||||
} else if (TEXT_EXTS.has(extOf(file.name))) {
|
||||
$('title').value = defaultFilename('text') || 'Text.txt';
|
||||
}
|
||||
const d = new Date();
|
||||
const iso = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
||||
const ext = extOf(raw || file.name);
|
||||
$('title').value = ext ? iso + '.' + ext : iso;
|
||||
}
|
||||
}
|
||||
showFileInEditor(file);
|
||||
|
||||
@@ -16,17 +16,25 @@ function toggleStats() {
|
||||
pill.classList.toggle('open', open);
|
||||
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
}
|
||||
function copyFeedback(btn) {
|
||||
if (!btn) return;
|
||||
if (!btn.dataset.label) btn.dataset.label = btn.textContent; // remember the original label (Copy/Link)
|
||||
btn.classList.add('ok');
|
||||
btn.textContent = 'Success!';
|
||||
clearTimeout(btn._okh);
|
||||
btn._okh = setTimeout(() => { btn.classList.remove('ok'); btn.textContent = btn.dataset.label; }, 2000);
|
||||
}
|
||||
function copyContent(btn) {
|
||||
navigator.clipboard.writeText(document.getElementById('raw-content').value);
|
||||
// in-place success feedback (#53)
|
||||
if (btn) {
|
||||
btn.classList.add('ok');
|
||||
btn.textContent = 'Success!';
|
||||
clearTimeout(btn._okh);
|
||||
btn._okh = setTimeout(() => { btn.classList.remove('ok'); btn.textContent = 'copy'; }, 2000);
|
||||
} else {
|
||||
toast('Link Copied', 'success');
|
||||
}
|
||||
navigator.clipboard.writeText(document.getElementById('raw-content').value)
|
||||
.then(() => copyFeedback(btn))
|
||||
.catch(() => toast('Copy failed'));
|
||||
}
|
||||
// #243: copy the paste LINK (full URL), not the paste id or content
|
||||
function copyLink(btn) {
|
||||
const url = location.origin + location.pathname;
|
||||
navigator.clipboard.writeText(url)
|
||||
.then(() => copyFeedback(btn))
|
||||
.catch(() => toast('Copy failed'));
|
||||
}
|
||||
function redeem() {
|
||||
if (!confirm('Hard delete this paste immediately?')) return;
|
||||
@@ -41,6 +49,8 @@ function redeem() {
|
||||
var PASTE_ID = document.currentScript.getAttribute('data-paste-id');
|
||||
var copyBtn = document.getElementById('copy-btn');
|
||||
if (copyBtn) copyBtn.addEventListener('click', function (e) { e.preventDefault(); copyContent(copyBtn); });
|
||||
var copyLinkBtn = document.getElementById('copy-link-btn');
|
||||
if (copyLinkBtn) copyLinkBtn.addEventListener('click', function (e) { e.preventDefault(); copyLink(copyLinkBtn); });
|
||||
var delBtn = document.getElementById('delete-btn');
|
||||
|
||||
// #168: show the compact paste-created pill in the bottom corner, then fade it out
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<option>markdown</option><option>text</option>
|
||||
</select>
|
||||
<button class="btn btn-icon" id="reguess" title="Re-detect language" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><polyline points="21 3 21 9 15 9"/></svg></button>
|
||||
<button type="button" class="btn btn-icon wrap-toggle" id="wrap-toggle" title="Toggle line wrap" aria-pressed="false">wrap</button>
|
||||
<button type="button" class="btn btn-icon wrap-toggle" id="wrap-toggle" title="Toggle line wrap" aria-pressed="false">Wrap</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
<h1>{{if .Title}}{{.Title}}{{else}}Untitled paste{{end}}</h1>
|
||||
{{if .CustomSlug}}<span class="slug">/{{.CustomSlug}}</span>{{end}}
|
||||
<div class="spacer"></div>
|
||||
<button type="button" class="iconbtn wrap-toggle" title="Toggle line wrap" aria-pressed="false">wrap</button>
|
||||
<a class="iconbtn" href="/raw/{{.ID}}">raw</a>
|
||||
<a class="iconbtn" href="#" id="copy-btn">copy</a>
|
||||
{{if .DeletionToken}}<a class="iconbtn danger" href="#" id="delete-btn">delete</a>{{end}}
|
||||
<button type="button" class="iconbtn wrap-toggle" title="Toggle line wrap" aria-pressed="false">Wrap</button>
|
||||
<a class="iconbtn" href="/raw/{{.ID}}">Raw</a>
|
||||
<a class="iconbtn" href="#" id="copy-link-btn">Link</a>
|
||||
<a class="iconbtn" href="#" id="copy-btn">Copy</a>
|
||||
{{if .DeletionToken}}<a class="iconbtn danger" href="#" id="delete-btn">Delete</a>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="float">
|
||||
|
||||
Reference in New Issue
Block a user