Skip to content

Commit 4f6fac7

Browse files
EvieePyLostLuma
andauthored
Feature/drag and drop and bugfix/handle-bad-paste-content (#39)
* Disable preloading until investigated further. * Update some docs * Add "/pastes" for API compat. * Possible theme flash fix. * Invalidate theme JS cache. * Possible speedups; Removed a fetch call and unnecessary CSS/JS loading. * Sadly this needs to be defered. * Remove all newlines from filenames * Add line numbers to single line pastes. * Allow HTML comments. * Added automatic files; removed add file button * Some CSS adjustments * Add favicon * Some CSS changes and fixes to new file behaviour. * Revert light theme paste background to white * Changes to README * More README changes. * Paste Header style changes * Add some annotations style changes * Various CSS adjustments; better spacing, light theme improvements. * Return eror if paste has invalid characters. * Add drag and drop files. --------- Co-authored-by: LostLuma <[email protected]>
1 parent 0e27a47 commit 4f6fac7

File tree

6 files changed

+187
-26
lines changed

6 files changed

+187
-26
lines changed

views/api.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import json
2323
from typing import TYPE_CHECKING, Any
2424

25+
import asyncpg
2526
import starlette_plus
2627

2728
from core import CONFIG
@@ -277,7 +278,11 @@ async def paste_post(self, request: starlette_plus.Request) -> starlette_plus.Re
277278
data["expires"] = expiry
278279
data["password"] = data.get("password")
279280

280-
paste = await self.app.database.create_paste(data=data)
281+
try:
282+
paste = await self.app.database.create_paste(data=data)
283+
except asyncpg.CharacterNotInRepertoireError:
284+
message: str = "File(s)/Filename(s) contain invalid characters or byte sequences."
285+
return starlette_plus.JSONResponse({"error": message}, status_code=400)
281286

282287
to_return: dict[str, Any] = paste.serialize(exclude=["password", "password_ok"])
283288
to_return.pop("files", None)

views/htmx.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from typing import TYPE_CHECKING, Any, cast
2424
from urllib.parse import unquote, urlsplit
2525

26+
import asyncpg
2627
import bleach
2728
import humanize
2829
import starlette_plus
@@ -257,8 +258,14 @@ async def htmx_save(self, request: starlette_plus.Request) -> starlette_plus.Res
257258

258259
inner: dict[str, str | None] = {}
259260

260-
inner["filename"] = names[n] or None
261-
inner["content"] = contents[n]
261+
try:
262+
inner["filename"] = names[n].encode("UTF-8").decode("UTF-8") or None
263+
inner["content"] = contents[n].encode("UTF-8").decode("UTF-8")
264+
except Exception:
265+
return starlette_plus.HTMLResponse(
266+
"""<span id="errorResponse">400: File/Filename contains invalid characters.</span>""",
267+
headers=error_headers,
268+
)
262269
data["files"].append(inner)
263270

264271
if not data["files"]:
@@ -277,7 +284,14 @@ async def htmx_save(self, request: starlette_plus.Request) -> starlette_plus.Res
277284
data["expires"] = None # TODO: Add this to Frontend...
278285
data["password"] = password or None
279286

280-
paste = await self.app.database.create_paste(data=data)
287+
try:
288+
paste = await self.app.database.create_paste(data=data)
289+
except asyncpg.CharacterNotInRepertoireError:
290+
return starlette_plus.HTMLResponse(
291+
"""<span id="errorResponse">400: File/Filename contains invalid characters.</span>""",
292+
headers=error_headers,
293+
)
294+
281295
to_return: dict[str, Any] = paste.serialize(exclude=["password", "password_ok"])
282296
identifier: str = to_return["id"]
283297

web/index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<script src="/static/scripts/initialTheme.js?v=1"></script>
1919
<script src="/static/scripts/themes.js?v=1" defer></script>
2020
<script src="/static/scripts/files.js?v=3" defer></script>
21+
<script src="/static/scripts/dragDrop.js?v=1"></script>
2122

2223
<!-- STYLESHEETS -->
2324
<!-- <link rel="preload" href="static/styles/global.css" as="style" /> -->
@@ -54,7 +55,7 @@
5455
<form id="content" class="content" hx-swap-oob="true" hx-swap="outerHTML"
5556
hx-include="[name='fileName'] [name='fileContent'] [name='pastePassword']">
5657
<div id="pastecontainer" class="pasteContainer">
57-
<div class="pasteArea" id="__file0" data-position="0">
58+
<div class="pasteArea" id="__file0" data-position="0" ondrop="fileDrop(event, this)" ondragover="fileDragOver(event, this)" ondragenter="fileDragStart(event, this)" ondragleave="fileDragEnd(event, this)">
5859
<div class="pasteHeader">
5960
<textarea name="fileName" class="filenameArea" rows="1" placeholder="Optional Filename..." maxlength="25"
6061
spellcheck="false"></textarea>
@@ -64,7 +65,7 @@
6465
Filling in all current files will create a new one (Up to 5)" maxlength="300000" onkeyup="addFile(0)"></textarea>
6566
</div>
6667

67-
<div class="pasteArea smallArea" id="__file1" data-position="1">
68+
<div class="pasteArea smallArea" id="__file1" data-position="1" ondrop="fileDrop(event, this)" ondragover="fileDragOver(event, this)" ondragenter="fileDragStart(event, this)" ondragleave="fileDragEnd(event, this)">
6869
<div class="pasteHeader">
6970
<textarea name="fileName" class="filenameArea" rows="1" placeholder="Optional Filename..." maxlength="25"
7071
spellcheck="false"></textarea>

web/static/scripts/dragDrop.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
let dragCounter = 0;
2+
3+
function fileDragStart(event, target) {
4+
event.preventDefault();
5+
event.stopPropagation();
6+
7+
target.classList.add("dragging");
8+
let pasteAreas = pasteContainer.getElementsByClassName("pasteArea");
9+
10+
for (let area of pasteAreas) {
11+
if (area === target) {
12+
continue;
13+
}
14+
area.classList.remove("dragging");
15+
}
16+
17+
if (target.classList.contains("smallArea")) {
18+
target.classList.remove("smallArea");
19+
}
20+
21+
dragCounter++;
22+
}
23+
24+
function fileDragOver(event, target) {
25+
event.preventDefault();
26+
27+
if (event.dataTransfer.items === 0) { return }
28+
29+
let type = event.dataTransfer.items[0].type;
30+
if (!type) { return }
31+
32+
if (!type.startsWith("text/") && !type.startsWith("application/")) {
33+
target.classList.add("prevented");
34+
event.dataTransfer.dropEffect = "none";
35+
}
36+
}
37+
38+
function fileDragEnd(event, target) {
39+
event.preventDefault();
40+
event.stopPropagation();
41+
42+
dragCounter--;
43+
if (dragCounter !== 0) { return }
44+
45+
target.classList.remove("dragging");
46+
target.classList.remove("prevented");
47+
48+
}
49+
50+
async function fileDrop(event, target) {
51+
event.preventDefault();
52+
event.stopPropagation();
53+
54+
dragCounter = 0;
55+
56+
target.classList.remove("prevented");
57+
target.classList.remove("dragging");
58+
59+
const file = event.dataTransfer.files[0];
60+
const textArea = target.querySelector(".fileContent");
61+
const fileName = target.querySelector(".pasteHeader > .filenameArea");
62+
63+
// Allow double the server limit incase of editing...
64+
if (file.size > 600000) {
65+
return;
66+
}
67+
68+
if (!file.type) { }
69+
else if (!file.type.startsWith("text/") && !file.type.startsWith("application/")) {
70+
return;
71+
}
72+
73+
let name = file.name;
74+
let content = await file.text();
75+
fileName.value = name;
76+
textArea.value = content;
77+
78+
addFile();
79+
}

web/static/scripts/files.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ function addFile(number) {
2929
count += 1;
3030

3131
const pasteHTML = `
32-
<div class="pasteArea smallArea" id="__file${count}" data-position="${count}">
32+
<div class="pasteArea smallArea" id="__file${count}" data-position="${count}" ondrop="fileDrop(event, this)" ondragover="fileDragOver(event, this)" ondragenter="fileDragStart(event, this)" ondragleave="fileDragEnd(event, this)">
33+
3334
<div class="pasteHeader">
3435
<textarea name="fileName" class="filenameArea" rows="1" placeholder="Optional Filename..." maxlength="25"></textarea>
3536
<span class="deleteFile" onclick="deleteFile('__file${count}')">Delete File</span>
@@ -84,7 +85,8 @@ function deleteFile(identifier) {
8485
if (!canContinue) { return }
8586

8687
const pasteHTML = `
87-
<div class="pasteArea smallArea" id="__file${count}" data-position="${count}">
88+
<div class="pasteArea smallArea" id="__file${count}" data-position="${count}" ondrop="fileDrop(event, this)" ondragover="fileDragOver(event, this)" ondragenter="fileDragStart(event, this)" ondragleave="fileDragEnd(event, this)">
89+
8890
<div class="pasteHeader">
8991
<textarea name="fileName" class="filenameArea" rows="1" placeholder="Optional Filename..." maxlength="25"></textarea>
9092
<span class="deleteFile" onclick="deleteFile('__file${count}')">Delete File</span>
@@ -93,5 +95,4 @@ function deleteFile(identifier) {
9395
</div>`;
9496

9597
pasteContainer.insertAdjacentHTML("beforeend", pasteHTML);
96-
9798
}

web/static/styles/global.css

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,34 @@
1616
--button--brightness: brightness(0.95);
1717
--button--brightness-hover: brightness(0.85);
1818
--button--brightness-active: brightness(0.95);
19+
20+
.deleteFile {
21+
background-color: var(--color-background);
22+
filter: brightness(0.99);
23+
}
24+
25+
.deleteFile:hover {
26+
cursor: pointer;
27+
filter: brightness(0.97);
28+
}
29+
30+
.deleteFile:active {
31+
filter: brightness(0.99);
32+
}
33+
34+
.savePaste {
35+
background-color: var(--color-background);
36+
filter: brightness(0.99);
37+
}
38+
39+
.savePaste:hover {
40+
cursor: pointer;
41+
filter: brightness(0.97);
42+
}
43+
44+
.savePaste:active {
45+
filter: brightness(0.99);
46+
}
1947
}
2048

2149
[data-theme="dark"] {
@@ -138,32 +166,60 @@ a {
138166
height: 100%;
139167
background-color: var(--color-background--pastes);
140168
border-radius: 0.25rem;
169+
border: 1px solid transparent;
170+
171+
}
172+
173+
.dragging {
174+
opacity: 0.5;
175+
border: 1px dashed var(--color-accent);
176+
position: relative;
177+
cursor: copy;
178+
background-color: var(--color-background--pastes);
179+
}
180+
181+
.dragging::after {
182+
pointer-events: none;
183+
content: "Drop File to Paste...";
184+
position: absolute;
185+
top: 50%;
186+
left: 50%;
187+
transform: translate(-50%, -50%);
188+
}
189+
190+
.prevented {
191+
border: 1px dashed var(--color-error);
192+
cursor: no-drop;
193+
}
194+
195+
.prevented::after {
196+
content: "File is not allowed...";
197+
color: var(--color-error);
198+
font-weight: 700;
199+
font-size: larger;
141200
}
142201

143202
.pasteHeader {
144203
background-color: var(--color-second-paste);
145204
border-radius: 0.25rem 0.2rem 0 0;
205+
display: flex;
206+
flex-direction: row;
207+
justify-content: space-between;
208+
align-items: center;
209+
padding: 0.5rem 1rem;
210+
width: 100%;
146211
}
147212

148213
.pasteContainer {
149214
display: flex;
150215
flex-direction: column;
151-
gap: 2rem;
216+
gap: 1rem;
152217
flex-grow: 1;
153218
width: 100%;
154219
max-width: 100%;
155220
border-radius: 0.25rem;
156221
}
157222

158-
.pasteHeader {
159-
display: flex;
160-
flex-direction: row;
161-
justify-content: space-between;
162-
align-items: center;
163-
padding: 1rem;
164-
width: 100%;
165-
}
166-
167223
input[type="password"] {
168224
background-color: var(--color-background--pastes) !important;
169225
color: var(--color-foreground) !important;
@@ -212,13 +268,12 @@ input[type="password"] {
212268

213269
.smallArea {
214270
flex-grow: 0;
215-
height: 7rem;
216-
margin: 0 0 6rem 0;
271+
height: 9rem;
217272
}
218273

219274
.smallArea>textarea {
220-
min-height: 7rem;
221-
height: 7rem;
275+
min-height: 4rem;
276+
height: 4rem;
222277
}
223278

224279
textarea::-webkit-resizer {
@@ -252,7 +307,7 @@ textarea {
252307

253308
.deleteFile {
254309
display: flex;
255-
padding: 1rem;
310+
padding: 0.5rem 1rem;
256311
border-radius: 0.25rem;
257312
background-color: var(--color-background--header);
258313
filter: brightness(0.8);
@@ -318,7 +373,7 @@ textarea {
318373

319374
.savePaste {
320375
display: flex;
321-
padding: 1rem 4rem;
376+
padding: 0.75rem 4rem;
322377
border-radius: 0.25rem;
323378
background-color: var(--color-background--button);
324379
filter: var(--button--brightness);
@@ -491,7 +546,12 @@ textarea {
491546
}
492547

493548
.savePaste {
494-
padding: 1rem;
549+
padding: 0.75rem;
550+
font-size: 0.8em;
551+
}
552+
553+
.deleteFile {
554+
padding: 0.5rem 1rem;
495555
font-size: 0.8em;
496556
}
497557

@@ -524,6 +584,7 @@ textarea {
524584
.header {
525585
padding: 1rem 0.5rem;
526586
}
587+
527588
.pasteArea>textarea {
528589
font-size: 0.8em;
529590
}

0 commit comments

Comments
 (0)