Compare commits
5
Commits
57a73e9c17
...
ff7e9d2a07
No files matched your search
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/*.rpyc": true,
|
||||||
|
"**/*.rpa": true,
|
||||||
|
"**/*.rpymc": true,
|
||||||
|
"**/cache/": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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__":
|
||||||
@@ -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,202 @@
|
|||||||
|
/* 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;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: fit-content;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
@@ -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,56 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:has( ~ .container .pastepad-container) {
|
||||||
|
max-width: 1000px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
.route-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
details:has(.route-list) {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; */
|
||||||
|
max-width: 800px;
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-grid li {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
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 {
|
||||||
|
word-break: break-word;
|
||||||
|
align-content: center;
|
||||||
|
/* flex: 1; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific override for the queue input form */
|
||||||
|
.queue-form {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-form textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar:has(~ .container .queue-title) {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.reader-btn {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 20px 40px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
.trail-next {
|
||||||
|
max-width: 80%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
@@ -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 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>
|
||||||
+14
-5
@@ -17,7 +17,7 @@
|
|||||||
<h2>Input (PC)</h2>
|
<h2>Input (PC)</h2>
|
||||||
<p>
|
<p>
|
||||||
Paste your OneTab export or list of links here to add them
|
Paste your OneTab export or list of links here to add them
|
||||||
to the queue.
|
to the queue
|
||||||
</p>
|
</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a href="/queuein" class="btn">Add Links</a>
|
<a href="/queuein" class="btn">Add Links</a>
|
||||||
@@ -26,17 +26,17 @@
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Output (Mobile)</h2>
|
<h2>Output (Mobile)</h2>
|
||||||
<p>Open these on your phone to consume the queue.</p>
|
<p>Open these on your phone to consume the queue</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a href="/q" class="btn">Reader (QR)</a>
|
<a href="/q" class="btn">Reader (QR)</a>
|
||||||
<a href="/t" class="btn">Trail (Focus)</a>
|
<a href="/t" class="btn">Trail (Focus)</a>
|
||||||
<a href="/r" class="btn">Next Link</a>
|
<a href="/r" class="btn">Next Link</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="card" style="margin-block: 1em">
|
<div class="card">
|
||||||
<h2>Scan this page</h2>
|
<h2>Scan this page</h2>
|
||||||
<p>Connect your phone to the same Wi-Fi and scan this.</p>
|
<p>Connect your phone to the same Wi-Fi and scan this</p>
|
||||||
<div
|
<div
|
||||||
class="qrcode"
|
class="qrcode"
|
||||||
style="padding: 1rem; background: white; border-radius: 8px"
|
style="padding: 1rem; background: white; border-radius: 8px"
|
||||||
@@ -54,6 +54,15 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>PastePad</h2>
|
||||||
|
<p>Paste your clipboard or any text and manage them!</p>
|
||||||
|
<div class="actions">
|
||||||
|
<a href="/pastepad" class="btn">Visit</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<details>
|
<details>
|
||||||
<summary>System Routes</summary>
|
<summary>System Routes</summary>
|
||||||
<div class="route-list">
|
<div class="route-list">
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ queue_items %}
|
|||||||
{% for item in queue_items %}
|
{% for item in queue_items %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ item }}" target="_blank" rel="noreferrer noopener">
|
<a href="{{ item }}" target="_blank" rel="noreferrer noopener">
|
||||||
{{ item }} {% if not loop.index > qr_limit %}
|
{{ item }}
|
||||||
|
</a>
|
||||||
|
{% if not loop.index > qr_limit %}
|
||||||
<div class="qrcode"></div>
|
<div class="qrcode"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
@@ -40,13 +41,17 @@ queue_items %}
|
|||||||
<script>
|
<script>
|
||||||
window.addEventListener("load", (_) => {
|
window.addEventListener("load", (_) => {
|
||||||
document.querySelectorAll(".qrcode").forEach((e) => {
|
document.querySelectorAll(".qrcode").forEach((e) => {
|
||||||
const a = e.parentElement.textContent.trim();
|
// Find the sibling <a> tag within the same <li>
|
||||||
|
const link = e.parentElement.querySelector("a");
|
||||||
|
const a = link ? link.textContent.trim() : "";
|
||||||
|
if (a) {
|
||||||
new QRCode(e, {
|
new QRCode(e, {
|
||||||
text: a,
|
text: a,
|
||||||
correctLevel: QRCode.CorrectLevel.L,
|
correctLevel: QRCode.CorrectLevel.L,
|
||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+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 %}
|
||||||
+6
-19
@@ -1,19 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %} {% block title %}QRCode queue{% endblock %} {% block
|
||||||
<html lang="en">
|
content %} {% from "macros.html" import qrcode_queue %} {% if queue_items %}
|
||||||
<head>
|
<p>total: {{ queue_items|length }}</p>
|
||||||
<meta charset="UTF-8" />
|
{{ qrcode_queue(queue_items) }} {% else %}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<p>empty</p>
|
||||||
<title>QRCode queue</title>
|
{% endif %} {% endblock %}
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="{{ url_for('static', path='style.css') }}"
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{% from "macros.html" import qrcode_queue %} {% if queue_items %}
|
|
||||||
<p>total: {{ queue_items|length }}</p>
|
|
||||||
{{ qrcode_queue(queue_items) }} {% else %}
|
|
||||||
<p>empty</p>
|
|
||||||
{% endif %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
+12
-36
@@ -1,41 +1,19 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %} {% block title %}Queue Input{% endblock %} {% block
|
||||||
<html lang="en">
|
content %}
|
||||||
<head>
|
<div class="queue-title">
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Queue Input</title>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="{{ url_for('static', path='style.css') }}"
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div
|
|
||||||
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>
|
||||||
<a
|
<a href="javascript:location.reload()" class="btn" style="margin: 0"
|
||||||
href="javascript:location.reload()"
|
|
||||||
class="btn"
|
|
||||||
style="margin: 0"
|
|
||||||
>Reload</a
|
>Reload</a
|
||||||
>
|
>
|
||||||
</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>
|
||||||
<script>
|
<script>
|
||||||
// lifted from google search ai
|
// lifted from google search ai
|
||||||
/**@type {HTMLTextAreaElement} */
|
/**@type {HTMLTextAreaElement} */
|
||||||
(document.getElementById("lines")).addEventListener(
|
(document.getElementById("lines")).addEventListener(
|
||||||
@@ -49,9 +27,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
{% from "macros.html" import qrcode_queue %} {% if queue_items %}
|
{% from "macros.html" import qrcode_queue %} {% if queue_items %}
|
||||||
<p>total: {{ queue_items|length }}</p>
|
<p>total: {{ queue_items|length }}</p>
|
||||||
{% endif %} {{ qrcode_queue(queue_items) }}
|
{% endif %} {{ qrcode_queue(queue_items) }} {% endblock %}
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
+3
-11
@@ -15,15 +15,7 @@
|
|||||||
<div class="trail-container">
|
<div class="trail-container">
|
||||||
<div class="trail-count">{{ queue_length }}</div>
|
<div class="trail-count">{{ queue_length }}</div>
|
||||||
<p>items left</p>
|
<p>items left</p>
|
||||||
|
<p class="trail-next">
|
||||||
<p
|
|
||||||
style="
|
|
||||||
max-width: 80%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Next:
|
Next:
|
||||||
<a
|
<a
|
||||||
href="{{ current_link }}"
|
href="{{ current_link }}"
|
||||||
@@ -39,7 +31,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
document.body.addEventListener("click", (e) => {
|
document.body.addEventListener("click", (e) => {
|
||||||
if (e.target.tagName === "A") return;
|
if (!e.target || e.target.tagName === "A") return;
|
||||||
// now this is not the new updated
|
// now this is not the new updated
|
||||||
const newTab = window.open("", "_blank");
|
const newTab = window.open("", "_blank");
|
||||||
|
|
||||||
@@ -54,7 +46,7 @@
|
|||||||
// Trigger the message event to run the swap on the next tick
|
// Trigger the message event to run the swap on the next tick
|
||||||
newTab.postMessage("");
|
newTab.postMessage("");
|
||||||
|
|
||||||
// background
|
// background. This block is racing with the slow internet.
|
||||||
if (newTab.document.body) {
|
if (newTab.document.body) {
|
||||||
newTab.document.body.style.backgroundColor =
|
newTab.document.body.style.backgroundColor =
|
||||||
getComputedStyle(document.body).backgroundColor;
|
getComputedStyle(document.body).backgroundColor;
|
||||||
|
|||||||
Reference in new issue
Block a user