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__":
|
||||
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"
|
||||
|
||||
from random import choice
|
||||
import socket
|
||||
import string
|
||||
from collections import deque
|
||||
from itertools import islice
|
||||
import string
|
||||
from random import choice
|
||||
from typing import Optional
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
from fastapi import FastAPI, Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
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
|
||||
|
||||
app = FastAPI()
|
||||
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
|
||||
|
||||
|
||||
class Paste(BaseModel):
|
||||
content: str
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/pastepad")
|
||||
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):
|
||||
return "".join(choice(s) for _ in range(l))
|
||||
|
||||
|
||||
@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]}
|
||||
def random_str(length: int = 8, charset=string.ascii_letters + string.digits):
|
||||
return "".join(choice(charset) for _ in range(length))
|
||||
|
||||
|
||||
@app.get("/api/pastepad")
|
||||
async def paste_query(name: str):
|
||||
if name in texts:
|
||||
async def paste_list():
|
||||
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": None, "content": None}
|
||||
|
||||
|
||||
@app.get("/api/pastepad/all")
|
||||
async def paste_dump():
|
||||
return list({"name": n, "content": c} for n, c in texts.items())
|
||||
@app.get("/api/pastepad/{name}")
|
||||
async def 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.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):
|
||||
if name in texts:
|
||||
t = texts[name]
|
||||
del texts[name]
|
||||
return {"name": name, "content": t}
|
||||
return {"name": None, "content": None}
|
||||
if name not in texts:
|
||||
raise HTTPException(status_code=404, detail="Paste not found")
|
||||
|
||||
|
||||
@app.put("/api/pastepad")
|
||||
@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}
|
||||
content = texts.pop(name)
|
||||
return {"name": name, "content": content}
|
||||
|
||||
|
||||
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 {
|
||||
--bg-color: #121212;
|
||||
--text-color: #e0e0e0;
|
||||
--primary-color: #bb86fc;
|
||||
--secondary-color: #03dac6;
|
||||
--error-color: #cf6679;
|
||||
--surface-color: #1e1e1e;
|
||||
--border-radius: 8px;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@import "css/variables.css";
|
||||
@import "css/base.css";
|
||||
@import "css/components.css";
|
||||
@import "css/pages/index.css";
|
||||
@import "css/pages/queue.css";
|
||||
@import "css/pages/trail.css";
|
||||
@import "css/pages/pastepad.css";
|
||||
@import "css/pages/reader.css";
|
||||
@@ -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>
|
||||
<p>
|
||||
Paste your OneTab export or list of links here to add them
|
||||
to the queue.
|
||||
to the queue
|
||||
</p>
|
||||
<div class="actions">
|
||||
<a href="/queuein" class="btn">Add Links</a>
|
||||
@@ -26,17 +26,17 @@
|
||||
|
||||
<div class="card">
|
||||
<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">
|
||||
<a href="/q" class="btn">Reader (QR)</a>
|
||||
<a href="/t" class="btn">Trail (Focus)</a>
|
||||
<a href="/r" class="btn">Next Link</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="margin-block: 1em">
|
||||
|
||||
<div class="card">
|
||||
<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
|
||||
class="qrcode"
|
||||
style="padding: 1rem; background: white; border-radius: 8px"
|
||||
@@ -54,6 +54,15 @@
|
||||
});
|
||||
</script>
|
||||
</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>
|
||||
<summary>System Routes</summary>
|
||||
<div class="route-list">
|
||||
|
||||
@@ -29,10 +29,11 @@ queue_items %}
|
||||
{% for item in queue_items %}
|
||||
<li>
|
||||
<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>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
@@ -40,13 +41,17 @@ queue_items %}
|
||||
<script>
|
||||
window.addEventListener("load", (_) => {
|
||||
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, {
|
||||
text: a,
|
||||
correctLevel: QRCode.CorrectLevel.L,
|
||||
width: 256,
|
||||
height: 256,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
+29
-21
@@ -1,22 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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");
|
||||
{% extends "base.html" %} {% block title %}PastePad - Queue Manager{% endblock
|
||||
%} {% block content %}
|
||||
<div class="pastepad-container">
|
||||
<h1>PastePad</h1>
|
||||
|
||||
/** @type {HTMLButtonElement} */
|
||||
(document.getElementById("s")).addEventListener("click", () => {});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<div class="editor card">
|
||||
<input
|
||||
id="n"
|
||||
placeholder="Custom Name (optional)"
|
||||
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>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>QRCode queue</title>
|
||||
<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>
|
||||
{% extends "base.html" %} {% block title %}QRCode queue{% endblock %} {% block
|
||||
content %} {% from "macros.html" import qrcode_queue %} {% if queue_items %}
|
||||
<p>total: {{ queue_items|length }}</p>
|
||||
{{ qrcode_queue(queue_items) }} {% else %}
|
||||
<p>empty</p>
|
||||
{% endif %} {% endblock %}
|
||||
+12
-36
@@ -1,41 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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;
|
||||
"
|
||||
>
|
||||
{% extends "base.html" %} {% block title %}Queue Input{% endblock %} {% block
|
||||
content %}
|
||||
<div class="queue-title">
|
||||
<h1 style="margin: 0">Add Links</h1>
|
||||
<div>
|
||||
<a href="/" class="btn" style="margin: 0">← Home</a>
|
||||
<a
|
||||
href="javascript:location.reload()"
|
||||
class="btn"
|
||||
style="margin: 0"
|
||||
<a href="javascript:location.reload()" class="btn" style="margin: 0"
|
||||
>Reload</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/queuein">
|
||||
</div>
|
||||
<form method="POST" action="/queuein" class="queue-form">
|
||||
<textarea id="lines" rows="20" cols="100" name="lines"></textarea>
|
||||
<input type="submit" value="ahh" />
|
||||
</form>
|
||||
<script>
|
||||
</form>
|
||||
<script>
|
||||
// lifted from google search ai
|
||||
/**@type {HTMLTextAreaElement} */
|
||||
(document.getElementById("lines")).addEventListener(
|
||||
@@ -49,9 +27,7 @@
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
{% from "macros.html" import qrcode_queue %} {% if queue_items %}
|
||||
<p>total: {{ queue_items|length }}</p>
|
||||
{% endif %} {{ qrcode_queue(queue_items) }}
|
||||
</body>
|
||||
</html>
|
||||
</script>
|
||||
{% from "macros.html" import qrcode_queue %} {% if queue_items %}
|
||||
<p>total: {{ queue_items|length }}</p>
|
||||
{% endif %} {{ qrcode_queue(queue_items) }} {% endblock %}
|
||||
+3
-11
@@ -15,15 +15,7 @@
|
||||
<div class="trail-container">
|
||||
<div class="trail-count">{{ queue_length }}</div>
|
||||
<p>items left</p>
|
||||
|
||||
<p
|
||||
style="
|
||||
max-width: 80%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
"
|
||||
>
|
||||
<p class="trail-next">
|
||||
Next:
|
||||
<a
|
||||
href="{{ current_link }}"
|
||||
@@ -39,7 +31,7 @@
|
||||
<script>
|
||||
window.addEventListener("load", () => {
|
||||
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
|
||||
const newTab = window.open("", "_blank");
|
||||
|
||||
@@ -54,7 +46,7 @@
|
||||
// Trigger the message event to run the swap on the next tick
|
||||
newTab.postMessage("");
|
||||
|
||||
// background
|
||||
// background. This block is racing with the slow internet.
|
||||
if (newTab.document.body) {
|
||||
newTab.document.body.style.backgroundColor =
|
||||
getComputedStyle(document.body).backgroundColor;
|
||||
|
||||
Reference in new issue
Block a user