heavylifting
breaking the trend to say that i fully let AI do the work here im not touching Shit lowk idek how this thing works. There was an initial different style for the paste pad, but idk man thats what you get for AI works. you dont know everything. You dont need to.
This commit is contained in:
1 parent
57a73e9c17
commit
107e880cf8
16 files changed
+906
-333
No files matched your search
@@ -0,0 +1,194 @@
|
|||||||
|
import socket
|
||||||
|
import string
|
||||||
|
from collections import deque
|
||||||
|
from itertools import islice
|
||||||
|
from random import choice
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Form, HTTPException, Request
|
||||||
|
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
app = FastAPI(title="Queue Manager")
|
||||||
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
# --- State ---
|
||||||
|
queue: deque[str] = deque()
|
||||||
|
texts: dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Models ---
|
||||||
|
class Paste(BaseModel):
|
||||||
|
content: str
|
||||||
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
def get_local_ip() -> str:
|
||||||
|
"""Determines the local IP address of the machine."""
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
try:
|
||||||
|
# doesn't even have to be reachable
|
||||||
|
s.connect(("10.255.255.255", 1))
|
||||||
|
IP = s.getsockname()[0]
|
||||||
|
except Exception:
|
||||||
|
IP = "127.0.0.1"
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
return IP
|
||||||
|
|
||||||
|
|
||||||
|
def link_extract_from_onetab(line: str) -> str:
|
||||||
|
"""Extracts URL from OneTab export format (URL | Title)."""
|
||||||
|
link = line.split("|", 1)[0].strip()
|
||||||
|
return urlunsplit(urlsplit(link))
|
||||||
|
|
||||||
|
|
||||||
|
def random_str(length: int = 8, charset=string.ascii_letters + string.digits) -> str:
|
||||||
|
"""Generates a random string."""
|
||||||
|
return "".join(choice(charset) for _ in range(length))
|
||||||
|
|
||||||
|
|
||||||
|
# --- Routes: System ---
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request):
|
||||||
|
"""Landing page with QR code."""
|
||||||
|
port = request.url.port or 2222
|
||||||
|
local_ip = get_local_ip()
|
||||||
|
page_url = f"{request.url.scheme}://{local_ip}:{port}"
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request, "index.html", {"routes": app.routes, "page_url": page_url}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Routes: Queue (Frontend) ---
|
||||||
|
@app.get("/queuein", response_class=HTMLResponse)
|
||||||
|
async def queue_in(request: Request):
|
||||||
|
"""Page to add links to the queue."""
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request, "queue.html", {"queue_items": list(queue)}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/queuein")
|
||||||
|
async def send_queue(lines: str = Form("")):
|
||||||
|
"""Handle form submission to add links."""
|
||||||
|
queue.extend(
|
||||||
|
link_extract_from_onetab(line) for line in lines.splitlines() if line.strip()
|
||||||
|
)
|
||||||
|
return RedirectResponse(url="/queuein", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/q", response_class=HTMLResponse)
|
||||||
|
async def reader_view(request: Request):
|
||||||
|
"""Reader view (QR Code per item)."""
|
||||||
|
current_link = queue[0] if queue else None
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
|
"reader.html",
|
||||||
|
{"current_link": current_link, "queue_length": len(queue)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/t", response_class=HTMLResponse)
|
||||||
|
async def trail_view(request: Request):
|
||||||
|
"""Trail view (Focus mode)."""
|
||||||
|
current_link = queue[0] if queue else None
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
|
"trail.html",
|
||||||
|
{"current_link": current_link, "queue_length": len(queue)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/r")
|
||||||
|
async def pop_and_redirect(request: Request):
|
||||||
|
"""Pop the next item and redirect to it."""
|
||||||
|
if queue:
|
||||||
|
q = queue.popleft()
|
||||||
|
return RedirectResponse(url=q)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request, "reader.html", {"current_link": None, "queue_length": len(queue)}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/qrcode_queue", response_class=HTMLResponse)
|
||||||
|
async def qrcode_queue_view(request: Request):
|
||||||
|
"""View entire queue as QR codes."""
|
||||||
|
return templates.TemplateResponse(request, "qq.html", {"queue_items": list(queue)})
|
||||||
|
|
||||||
|
|
||||||
|
# --- Routes: Queue (API) ---
|
||||||
|
@app.get("/api/queue")
|
||||||
|
async def api_queue_list(n: int | None = None, s: int | None = None):
|
||||||
|
return list(islice(queue, s, n))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/queue/len")
|
||||||
|
async def api_queue_len():
|
||||||
|
return len(queue)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/queue/clear")
|
||||||
|
async def api_queue_clear():
|
||||||
|
queue.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Routes: PastePad ---
|
||||||
|
@app.get("/pastepad", response_class=HTMLResponse)
|
||||||
|
async def pastepad_ui(request: Request):
|
||||||
|
"""PastePad UI."""
|
||||||
|
return templates.TemplateResponse(request, "pastepad.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/pastepad")
|
||||||
|
async def api_paste_list():
|
||||||
|
return [{"name": n, "content": c} for n, c in texts.items()]
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/pastepad", status_code=201)
|
||||||
|
async def api_paste_create(paste: Paste):
|
||||||
|
if paste.name and paste.name in texts:
|
||||||
|
raise HTTPException(status_code=409, detail="Paste name already exists")
|
||||||
|
|
||||||
|
name = paste.name or random_str()
|
||||||
|
while name in texts:
|
||||||
|
name = random_str()
|
||||||
|
|
||||||
|
texts[name] = paste.content
|
||||||
|
return {"name": name, "content": texts[name]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/pastepad/{name}")
|
||||||
|
async def api_paste_get(name: str):
|
||||||
|
if name not in texts:
|
||||||
|
raise HTTPException(status_code=404, detail="Paste not found")
|
||||||
|
return {"name": name, "content": texts[name]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/pastepad/{name}/raw", response_class=PlainTextResponse)
|
||||||
|
async def api_paste_get_raw(name: str):
|
||||||
|
if name not in texts:
|
||||||
|
raise HTTPException(status_code=404, detail="Paste not found")
|
||||||
|
return texts[name]
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/pastepad/{name}")
|
||||||
|
async def api_paste_update(name: str, paste: Paste):
|
||||||
|
if name not in texts:
|
||||||
|
raise HTTPException(status_code=404, detail="Paste not found")
|
||||||
|
texts[name] = paste.content
|
||||||
|
return {"name": name, "content": texts[name]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/pastepad/{name}")
|
||||||
|
async def api_paste_delete(name: str):
|
||||||
|
if name not in texts:
|
||||||
|
raise HTTPException(status_code=404, detail="Paste not found")
|
||||||
|
content = texts.pop(name)
|
||||||
|
return {"name": name, "content": content}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
uvicorn.run("uhhhhup:app", host="0.0.0.0", port=2222, reload=True)
|
uvicorn.run("app:app", host="0.0.0.0", port=2222, reload=True)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
body {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100dvh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/* Buttons */
|
||||||
|
button,
|
||||||
|
input[type="submit"],
|
||||||
|
.btn {
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background-color 0.2s, color 0.2s;
|
||||||
|
margin: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover,
|
||||||
|
input[type="submit"]:hover,
|
||||||
|
.btn:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--bg-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
border-color: var(--error-color);
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: var(--error-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
border-color: #666;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
textarea {
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
/* max-width: 800px; Removed global max-width */
|
||||||
|
font-family: monospace;
|
||||||
|
min-width: 20%;
|
||||||
|
min-height: 20dvh;
|
||||||
|
box-sizing: border-box; /* Ensure padding doesn't break width */
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* align-items: center; Removed global alignment */
|
||||||
|
width: 100%;
|
||||||
|
/* max-width: 800px; Removed global max-width */
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.form-control {
|
||||||
|
min-height: 150px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card {
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* align-items: center; Removed to allow full width content */
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #333;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar */
|
||||||
|
.navbar {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
/* Generic list styles can go here if needed */
|
||||||
|
|
||||||
|
/* Details/Summary */
|
||||||
|
details {
|
||||||
|
width: 100%;
|
||||||
|
/* max-width: 600px; Removed global max-width */
|
||||||
|
margin-top: 20px;
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
padding: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary::after {
|
||||||
|
content: "+";
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] summary::after {
|
||||||
|
content: "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] summary {
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary::marker,
|
||||||
|
summary::-webkit-details-marker {
|
||||||
|
list-style-type: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc */
|
||||||
|
.qrcode {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-inline: auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.hero {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
.pastepad-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px; /* Increased from 800px */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-item {
|
||||||
|
background: var(--surface-color);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
text-align: left; /* Override card's center alignment */
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
width: 100%; /* Ensure full width */
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-item pre {
|
||||||
|
background: #111;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
width: 100%; /* Ensure full width */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
.route-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
width: 800px;
|
||||||
|
padding: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-item {
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-family: monospace;
|
||||||
|
gap: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 800px;
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-grid li {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
/* Visual styles moved from components.css */
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-grid li a {
|
||||||
|
align-items: center;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific override for the queue input form */
|
||||||
|
.queue-form {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-form textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.reader-btn {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 20px 40px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.trail-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 80dvh;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trail-count {
|
||||||
|
font-size: 5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trail-hint {
|
||||||
|
color: #666;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
:root {
|
||||||
|
--bg-color: #121212;
|
||||||
|
--text-color: #e0e0e0;
|
||||||
|
--primary-color: #bb86fc;
|
||||||
|
--secondary-color: #03dac6;
|
||||||
|
--error-color: #cf6679;
|
||||||
|
--surface-color: #1e1e1e;
|
||||||
|
--border-radius: 8px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
// --- DOM Elements ---
|
||||||
|
const elements = {
|
||||||
|
nameInput: document.getElementById("n"),
|
||||||
|
contentInput: document.getElementById("pc"),
|
||||||
|
saveBtn: document.getElementById("s"),
|
||||||
|
listDiv: document.getElementById("list"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- State Management ---
|
||||||
|
const state = {
|
||||||
|
isEditMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- API Interactions ---
|
||||||
|
const api = {
|
||||||
|
async getPastes() {
|
||||||
|
const res = await fetch("/api/pastepad");
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch pastes");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPaste(name) {
|
||||||
|
const res = await fetch(`/api/pastepad/${name}`);
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch paste");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async createPaste(payload) {
|
||||||
|
const res = await fetch("/api/pastepad", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
throw new Error(err.detail || "Failed to create paste");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async updatePaste(name, payload) {
|
||||||
|
const res = await fetch(`/api/pastepad/${name}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
throw new Error(err.detail || "Failed to update paste");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async deletePaste(name) {
|
||||||
|
const res = await fetch(`/api/pastepad/${name}`, { method: "DELETE" });
|
||||||
|
if (!res.ok) throw new Error("Failed to delete paste");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- UI Functions ---
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return text;
|
||||||
|
return text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPasteItem(p) {
|
||||||
|
const safeName = escapeHtml(p.name);
|
||||||
|
const safeContent = escapeHtml(p.content);
|
||||||
|
return `
|
||||||
|
<div class="paste-item card">
|
||||||
|
<div class="paste-header">
|
||||||
|
<strong>${safeName}</strong>
|
||||||
|
<div class="actions">
|
||||||
|
<button onclick="editPaste('${safeName}')" class="btn-sm">Edit</button>
|
||||||
|
<a href="/api/pastepad/${safeName}/raw" target="_blank" class="btn-sm">Raw</a>
|
||||||
|
<button onclick="deletePaste('${safeName}')" class="btn-sm btn-danger">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre>${safeContent}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPastes() {
|
||||||
|
try {
|
||||||
|
const pastes = await api.getPastes();
|
||||||
|
elements.listDiv.innerHTML = pastes.map(renderPasteItem).join("");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
elements.listDiv.innerHTML = `<p style="color: var(--error-color)">Error loading pastes: ${e.message}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditMode(isEdit) {
|
||||||
|
state.isEditMode = isEdit;
|
||||||
|
const { nameInput, contentInput, saveBtn } = elements;
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
nameInput.readOnly = true;
|
||||||
|
nameInput.classList.add("disabled");
|
||||||
|
saveBtn.textContent = "Update Paste";
|
||||||
|
saveBtn.dataset.mode = "update";
|
||||||
|
|
||||||
|
if (!document.getElementById("cancelBtn")) {
|
||||||
|
const cancelBtn = document.createElement("button");
|
||||||
|
cancelBtn.id = "cancelBtn";
|
||||||
|
cancelBtn.textContent = "Cancel";
|
||||||
|
cancelBtn.className = "btn btn-secondary";
|
||||||
|
cancelBtn.onclick = resetForm;
|
||||||
|
saveBtn.after(cancelBtn);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nameInput.value = "";
|
||||||
|
contentInput.value = "";
|
||||||
|
nameInput.readOnly = false;
|
||||||
|
nameInput.classList.remove("disabled");
|
||||||
|
saveBtn.textContent = "Save Paste";
|
||||||
|
saveBtn.dataset.mode = "create";
|
||||||
|
const c = document.getElementById("cancelBtn");
|
||||||
|
if (c) c.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
setEditMode(false);
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.delete("name");
|
||||||
|
window.history.pushState({}, "", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
const content = elements.contentInput.value;
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
const name = elements.nameInput.value;
|
||||||
|
const payload = { content };
|
||||||
|
if (name) payload.name = name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (state.isEditMode) {
|
||||||
|
await api.updatePaste(name, payload);
|
||||||
|
} else {
|
||||||
|
await api.createPaste(payload);
|
||||||
|
}
|
||||||
|
resetForm();
|
||||||
|
loadPastes();
|
||||||
|
} catch (e) {
|
||||||
|
alert("Error: " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Initialization & Event Listeners ---
|
||||||
|
elements.saveBtn.addEventListener("click", handleSave);
|
||||||
|
|
||||||
|
// Global handlers for inline onclicks
|
||||||
|
window.editPaste = async (name) => {
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set("name", name);
|
||||||
|
window.history.pushState({}, "", url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await api.getPaste(name);
|
||||||
|
elements.nameInput.value = data.name;
|
||||||
|
elements.contentInput.value = data.content;
|
||||||
|
setEditMode(true);
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Could not load paste for editing");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.deletePaste = async (name) => {
|
||||||
|
if (!confirm("Delete " + name + "?")) return;
|
||||||
|
try {
|
||||||
|
await api.deletePaste(name);
|
||||||
|
loadPastes();
|
||||||
|
if (elements.nameInput.value === name) resetForm();
|
||||||
|
} catch (e) {
|
||||||
|
alert("Error deleting paste: " + e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check URL for edit mode on load
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const nameToEdit = params.get("name");
|
||||||
|
if (nameToEdit) {
|
||||||
|
window.editPaste(nameToEdit);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPastes();
|
||||||
|
});
|
||||||
+8
-262
@@ -1,262 +1,8 @@
|
|||||||
:root {
|
@import "css/variables.css";
|
||||||
--bg-color: #121212;
|
@import "css/base.css";
|
||||||
--text-color: #e0e0e0;
|
@import "css/components.css";
|
||||||
--primary-color: #bb86fc;
|
@import "css/pages/index.css";
|
||||||
--secondary-color: #03dac6;
|
@import "css/pages/queue.css";
|
||||||
--error-color: #cf6679;
|
@import "css/pages/trail.css";
|
||||||
--surface-color: #1e1e1e;
|
@import "css/pages/pastepad.css";
|
||||||
--border-radius: 8px;
|
@import "css/pages/reader.css";
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100dvh;
|
|
||||||
box-sizing: border-box; /* Ensure padding is included in width/height */
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
input[type="submit"],
|
|
||||||
.btn {
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
color: var(--primary-color);
|
|
||||||
border: 1px solid var(--primary-color);
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: background-color 0.2s, color 0.2s;
|
|
||||||
margin: 5px;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover,
|
|
||||||
input[type="submit"]:hover,
|
|
||||||
.btn:hover {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--bg-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 10px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
font-family: monospace;
|
|
||||||
min-width: 20%;
|
|
||||||
min-height: 20dvh;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
word-break: break-all;
|
|
||||||
width: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
li a {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qrcode {
|
|
||||||
margin-top: 10px;
|
|
||||||
background-color: white;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Specifics for reader */
|
|
||||||
.reader-btn {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
padding: 20px 40px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Specifics for index */
|
|
||||||
.hero {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 20px;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1000px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
flex: 1;
|
|
||||||
min-width: 300px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid #333;
|
|
||||||
/* max-height: 90dvh; Prevent card from exceeding viewport height */
|
|
||||||
overflow-y: auto; /* Scroll if content is too tall */
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
margin-top: 0;
|
|
||||||
/* margin-bottom: 10px; Reduced margin */
|
|
||||||
/* font-size: 1.2rem; Slightly smaller title */
|
|
||||||
}
|
|
||||||
|
|
||||||
.card p {
|
|
||||||
margin-bottom: 15px; /* Reduced margin */
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card .actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
details {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
margin-top: 20px;
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary {
|
|
||||||
padding: 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--secondary-color);
|
|
||||||
list-style: none; /* Hide default triangle */
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary::after {
|
|
||||||
content: "+";
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
details[open] summary::after {
|
|
||||||
content: "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
details[open] summary {
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary::marker,
|
|
||||||
summary::-webkit-details-marker {
|
|
||||||
list-style-type: none;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.route-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 15px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.route-item {
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Queue Grid */
|
|
||||||
.queue-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 20px;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-grid li {
|
|
||||||
flex: 1 1 300px; /* Grow, shrink, basis */
|
|
||||||
max-width: 400px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-grid li a {
|
|
||||||
align-items: center;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Trail Page */
|
|
||||||
.trail-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 80dvh;
|
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trail-count {
|
|
||||||
font-size: 5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.trail-hint {
|
|
||||||
color: #666;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{% block title %}Queue Manager{% endblock %}</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('static', path='style.css') }}"
|
||||||
|
/>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar">
|
||||||
|
<a href="/" class="nav-brand">QM</a>
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="/queuein">Queue</a>
|
||||||
|
<a href="/pastepad">PastePad</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="container">{% block content %}{% endblock %}</main>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+29
-21
@@ -1,22 +1,30 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %} {% block title %}PastePad - Queue Manager{% endblock
|
||||||
<html lang="en">
|
%} {% block content %}
|
||||||
<head>
|
<div class="pastepad-container">
|
||||||
<meta charset="UTF-8" />
|
<h1>PastePad</h1>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>paste pad</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<input id="n" />
|
|
||||||
<textarea id="pc"></textarea>
|
|
||||||
<button type="button" id="s">sendit</button>
|
|
||||||
<script>
|
|
||||||
/** @type {HTMLInputElement} */
|
|
||||||
const name = document.getElementById("n");
|
|
||||||
/** @type {HTMLTextAreaElement} */
|
|
||||||
const content = document.getElementById("pc");
|
|
||||||
|
|
||||||
/** @type {HTMLButtonElement} */
|
<div class="editor card">
|
||||||
(document.getElementById("s")).addEventListener("click", () => {});
|
<input
|
||||||
</script>
|
id="n"
|
||||||
</body>
|
placeholder="Custom Name (optional)"
|
||||||
</html>
|
class="form-control"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
id="pc"
|
||||||
|
placeholder="Paste content here..."
|
||||||
|
class="form-control"
|
||||||
|
></textarea>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<button type="button" id="s" class="btn btn-primary">
|
||||||
|
Save Paste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="list" class="paste-list">
|
||||||
|
<!-- Populated by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %} {% block scripts %}
|
||||||
|
<script src="{{ url_for('static', path='js/pastepad.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
+2
-11
@@ -10,16 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div
|
<div class="queue-title">
|
||||||
style="
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<h1 style="margin: 0">Add Links</h1>
|
<h1 style="margin: 0">Add Links</h1>
|
||||||
<div>
|
<div>
|
||||||
<a href="/" class="btn" style="margin: 0">← Home</a>
|
<a href="/" class="btn" style="margin: 0">← Home</a>
|
||||||
@@ -31,7 +22,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="/queuein">
|
<form method="POST" action="/queuein" class="queue-form">
|
||||||
<textarea id="lines" rows="20" cols="100" name="lines"></textarea>
|
<textarea id="lines" rows="20" cols="100" name="lines"></textarea>
|
||||||
<input type="submit" value="ahh" />
|
<input type="submit" value="ahh" />
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+56
-38
@@ -1,17 +1,18 @@
|
|||||||
"gemini converted"
|
"gemini converted"
|
||||||
|
|
||||||
from random import choice
|
|
||||||
import socket
|
import socket
|
||||||
|
import string
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from itertools import islice
|
from itertools import islice
|
||||||
import string
|
from random import choice
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
from fastapi import FastAPI, Form, Request
|
from fastapi import FastAPI, Form, HTTPException, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
@@ -128,54 +129,71 @@ async def send_queue(lines: str = Form("")):
|
|||||||
texts: dict[str, str] = {} # name (auto or rewrite): content
|
texts: dict[str, str] = {} # name (auto or rewrite): content
|
||||||
|
|
||||||
|
|
||||||
|
class Paste(BaseModel):
|
||||||
|
content: str
|
||||||
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/pastepad")
|
@app.get("/pastepad")
|
||||||
async def pastepad_ui(request: Request):
|
async def pastepad_ui(request: Request):
|
||||||
return templates.TemplateResponse(request, "pastepad.html", {"texts": texts})
|
return templates.TemplateResponse(request, "pastepad.html")
|
||||||
|
|
||||||
|
|
||||||
def random_str(l: int = 8, s=string.ascii_letters + string.digits):
|
def random_str(length: int = 8, charset=string.ascii_letters + string.digits):
|
||||||
return "".join(choice(s) for _ in range(l))
|
return "".join(choice(charset) for _ in range(length))
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/pastepad")
|
|
||||||
async def paste_in(content: str, name: Optional[str] = None):
|
|
||||||
if name in texts:
|
|
||||||
return {"name": name, "content": texts[name]}
|
|
||||||
if name is None:
|
|
||||||
name = random_str()
|
|
||||||
texts[name] = content
|
|
||||||
return {"name": name, "content": texts[name]}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/pastepad")
|
@app.get("/api/pastepad")
|
||||||
async def paste_query(name: str):
|
async def paste_list():
|
||||||
if name in texts:
|
return [{"name": n, "content": c} for n, c in texts.items()]
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/pastepad", status_code=201)
|
||||||
|
async def paste_create(paste: Paste):
|
||||||
|
"""Create a new paste. If name is provided and exists, returns 409."""
|
||||||
|
if paste.name and paste.name in texts:
|
||||||
|
raise HTTPException(status_code=409, detail="Paste name already exists")
|
||||||
|
|
||||||
|
name = paste.name or random_str()
|
||||||
|
while name in texts: # collision check for random
|
||||||
|
name = random_str()
|
||||||
|
|
||||||
|
texts[name] = paste.content
|
||||||
return {"name": name, "content": texts[name]}
|
return {"name": name, "content": texts[name]}
|
||||||
return {"name": None, "content": None}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/pastepad/all")
|
@app.get("/api/pastepad/{name}")
|
||||||
async def paste_dump():
|
async def paste_get(name: str):
|
||||||
return list({"name": n, "content": c} for n, c in texts.items())
|
if name not in texts:
|
||||||
|
raise HTTPException(status_code=404, detail="Paste not found")
|
||||||
|
return {"name": name, "content": texts[name]}
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/pastepad")
|
@app.get("/api/pastepad/{name}/raw", response_class=PlainTextResponse)
|
||||||
|
async def paste_get_raw(name: str):
|
||||||
|
"""Return just the raw text content, useful for curl or scripts."""
|
||||||
|
if name not in texts:
|
||||||
|
raise HTTPException(status_code=404, detail="Paste not found")
|
||||||
|
return texts[name]
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/pastepad/{name}")
|
||||||
|
async def paste_update(name: str, paste: Paste):
|
||||||
|
"""Update an existing paste."""
|
||||||
|
if name not in texts:
|
||||||
|
raise HTTPException(status_code=404, detail="Paste not found")
|
||||||
|
|
||||||
|
texts[name] = paste.content
|
||||||
|
return {"name": name, "content": texts[name]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/pastepad/{name}")
|
||||||
async def paste_delete(name: str):
|
async def paste_delete(name: str):
|
||||||
if name in texts:
|
if name not in texts:
|
||||||
t = texts[name]
|
raise HTTPException(status_code=404, detail="Paste not found")
|
||||||
del texts[name]
|
|
||||||
return {"name": name, "content": t}
|
|
||||||
return {"name": None, "content": None}
|
|
||||||
|
|
||||||
|
content = texts.pop(name)
|
||||||
@app.put("/api/pastepad")
|
return {"name": name, "content": content}
|
||||||
@app.patch("/api/pastepad")
|
|
||||||
async def paste_put(name: str, content: str):
|
|
||||||
if name in texts:
|
|
||||||
t = texts[name]
|
|
||||||
texts[name] = content
|
|
||||||
return {"name": name, "content": t}
|
|
||||||
return {"name": None, "content": None}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in new issue
Block a user