manage
This commit is contained in:
1 parent
1911d55d5e
commit
58d3e58759
7 files changed
+232
-53
No files matched your search
@@ -30,6 +30,10 @@ class Paste(BaseModel):
|
|||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractRequest(BaseModel):
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
# --- Helpers ---
|
# --- Helpers ---
|
||||||
def get_local_ip() -> str:
|
def get_local_ip() -> str:
|
||||||
"""Determines the local IP address of the machine."""
|
"""Determines the local IP address of the machine."""
|
||||||
@@ -119,10 +123,35 @@ async def pop_and_redirect(request: Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/qrcode_queue", response_class=HTMLResponse)
|
@app.get("/manage", response_class=HTMLResponse)
|
||||||
async def qrcode_queue_view(request: Request):
|
async def manage_view(request: Request):
|
||||||
"""View entire queue as QR codes."""
|
"""Management dashboard for the queue."""
|
||||||
return templates.TemplateResponse(request, "qq.html", {"queue_items": list(queue)})
|
return templates.TemplateResponse(
|
||||||
|
request, "manage.html", {"queue_items": list(queue)}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/queue/dedup")
|
||||||
|
async def api_queue_dedup():
|
||||||
|
"""Deduplicate the queue."""
|
||||||
|
global queue
|
||||||
|
# Preserve order while removing duplicates
|
||||||
|
seen = set()
|
||||||
|
new_queue = deque()
|
||||||
|
for item in queue:
|
||||||
|
if item not in seen:
|
||||||
|
seen.add(item)
|
||||||
|
new_queue.append(item)
|
||||||
|
queue = new_queue
|
||||||
|
return {"count": len(queue)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/queue/sort")
|
||||||
|
async def api_queue_sort(reverse: bool = False):
|
||||||
|
"""Sort the queue."""
|
||||||
|
global queue
|
||||||
|
queue = deque(sorted(queue, reverse=reverse))
|
||||||
|
return {"count": len(queue)}
|
||||||
|
|
||||||
|
|
||||||
# --- Routes: Queue (API) ---
|
# --- Routes: Queue (API) ---
|
||||||
@@ -213,6 +242,12 @@ async def extractor_ui(request: Request):
|
|||||||
return templates.TemplateResponse(request, "extractor.html")
|
return templates.TemplateResponse(request, "extractor.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/extract/jumblang")
|
||||||
|
async def api_extract_jumblang(request: ExtractRequest):
|
||||||
|
"""Extract URLs from messy text using urlextract."""
|
||||||
|
return list(extract_urls_from_jumblang(request.text))
|
||||||
|
|
||||||
|
|
||||||
def encode_safe(link: str) -> str:
|
def encode_safe(link: str) -> str:
|
||||||
return base64.urlsafe_b64encode(link.encode()).rstrip(b"=").decode()
|
return base64.urlsafe_b64encode(link.encode()).rstrip(b"=").decode()
|
||||||
|
|
||||||
@@ -225,5 +260,5 @@ def decode_safe(should_be_b64: str) -> str:
|
|||||||
|
|
||||||
def extract_urls_from_jumblang(txt: str) -> Iterator[str]:
|
def extract_urls_from_jumblang(txt: str) -> Iterator[str]:
|
||||||
return cast(
|
return cast(
|
||||||
Iterator[str], urlextract.URLExtract().gen_urls(txt)
|
Iterator[str], urlextract.URLExtract().gen_urls(txt, with_schema_only=True)
|
||||||
) # i read the code. PLEASE have overloads mr coders
|
) # i read the code. PLEASE have overloads mr coders
|
||||||
@@ -56,7 +56,8 @@ textarea {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
min-width: 20%;
|
min-width: 20%;
|
||||||
min-height: 20dvh;
|
min-height: 20dvh;
|
||||||
box-sizing: border-box; /* Ensure padding doesn't break width */
|
box-sizing: border-box;
|
||||||
|
/* Ensure padding doesn't break width */
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
@@ -123,7 +124,8 @@ textarea.form-control {
|
|||||||
/* Navbar */
|
/* Navbar */
|
||||||
.navbar {
|
.navbar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px; /* Default width */
|
max-width: 800px;
|
||||||
|
/* Default width */
|
||||||
background-color: var(--surface-color);
|
background-color: var(--surface-color);
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -201,3 +203,7 @@ summary::-webkit-details-marker {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qrcode:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
+57
-18
@@ -14,6 +14,7 @@ body_class %}layout-wide{% endblock %}{% block content %}
|
|||||||
id="file-upload"
|
id="file-upload"
|
||||||
style="display: none"
|
style="display: none"
|
||||||
accept=".html,.htm,.md,.txt,.markdown"
|
accept=".html,.htm,.md,.txt,.markdown"
|
||||||
|
multiple
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
id="file-name"
|
id="file-name"
|
||||||
@@ -34,6 +35,9 @@ body_class %}layout-wide{% endblock %}{% block content %}
|
|||||||
<button onclick="extractLinks('markdown')" class="btn">
|
<button onclick="extractLinks('markdown')" class="btn">
|
||||||
Extract from Markdown
|
Extract from Markdown
|
||||||
</button>
|
</button>
|
||||||
|
<button onclick="extractLinks('jumblang')" class="btn">
|
||||||
|
Extract from Messy Text
|
||||||
|
</button>
|
||||||
<button onclick="extractLinks('auto')" class="btn btn-primary">
|
<button onclick="extractLinks('auto')" class="btn btn-primary">
|
||||||
Auto Detect & Extract
|
Auto Detect & Extract
|
||||||
</button>
|
</button>
|
||||||
@@ -82,40 +86,60 @@ body_class %}layout-wide{% endblock %}{% block content %}
|
|||||||
/** @type {HTMLInputElement} */
|
/** @type {HTMLInputElement} */
|
||||||
const fileInput = document.getElementById("file-upload");
|
const fileInput = document.getElementById("file-upload");
|
||||||
|
|
||||||
fileInput.addEventListener("change", function (e) {
|
fileInput.addEventListener("change", async function (e) {
|
||||||
/** @type {File} */
|
/** @type {FileList} */
|
||||||
const file = e.target.files[0];
|
const files = e.target.files;
|
||||||
if (!file) return;
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
document.getElementById("file-name").textContent = file.name;
|
document.getElementById(
|
||||||
|
"file-name"
|
||||||
|
).textContent = `${files.length} file(s) selected`;
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function (e) {
|
|
||||||
/** @type {HTMLTextAreaElement} */
|
/** @type {HTMLTextAreaElement} */
|
||||||
const textarea = document.getElementById("input-content");
|
const textarea = document.getElementById("input-content");
|
||||||
textarea.value = e.target.result;
|
textarea.value = ""; // Clear previous content
|
||||||
|
|
||||||
// Optional: Auto-detect mode based on extension
|
// Read all files
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
const text = await readFileAsText(file);
|
||||||
|
textarea.value += `\n\n<!-- File: ${file.name} -->\n${text}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect mode based on the first file's extension (heuristic)
|
||||||
|
const firstFile = files[0];
|
||||||
if (
|
if (
|
||||||
file.name.endsWith(".md") ||
|
firstFile.name.endsWith(".md") ||
|
||||||
file.name.endsWith(".markdown")
|
firstFile.name.endsWith(".markdown")
|
||||||
) {
|
) {
|
||||||
extractLinks("markdown");
|
extractLinks("markdown");
|
||||||
} else if (
|
} else if (
|
||||||
file.name.endsWith(".html") ||
|
firstFile.name.endsWith(".html") ||
|
||||||
file.name.endsWith(".htm")
|
firstFile.name.endsWith(".htm")
|
||||||
) {
|
) {
|
||||||
extractLinks("html");
|
extractLinks("html");
|
||||||
}
|
}
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts links from the input content based on the specified mode.
|
* Reads a file as text using a Promise wrapper around FileReader.
|
||||||
* @param {'html' | 'markdown' | 'auto'} mode
|
* @param {File} file
|
||||||
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
function extractLinks(mode) {
|
function readFileAsText(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => resolve(e.target.result);
|
||||||
|
reader.onerror = (e) => reject(e);
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts links from the input content based on the specified mode.
|
||||||
|
* @param {'html' | 'markdown' | 'auto' | 'jumblang'} mode
|
||||||
|
*/
|
||||||
|
async function extractLinks(mode) {
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
const content = document.getElementById("input-content").value;
|
const content = document.getElementById("input-content").value;
|
||||||
if (!content.trim()) return;
|
if (!content.trim()) return;
|
||||||
@@ -123,6 +147,20 @@ body_class %}layout-wide{% endblock %}{% block content %}
|
|||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
let links = [];
|
let links = [];
|
||||||
|
|
||||||
|
if (mode === "jumblang") {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/extract/jumblang", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ text: content }),
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error("Extraction failed");
|
||||||
|
links = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
alert("Extraction failed: " + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (mode === "auto") {
|
if (mode === "auto") {
|
||||||
// Simple heuristic: if it has <a href, it's likely HTML.
|
// Simple heuristic: if it has <a href, it's likely HTML.
|
||||||
mode =
|
mode =
|
||||||
@@ -156,6 +194,7 @@ body_class %}layout-wide{% endblock %}{% block content %}
|
|||||||
// This is a bit aggressive, so maybe optional? For now let's stick to explicit MD syntax
|
// This is a bit aggressive, so maybe optional? For now let's stick to explicit MD syntax
|
||||||
// or use a library if we want full robustness.
|
// or use a library if we want full robustness.
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Deduplicate
|
// Deduplicate
|
||||||
links = [...new Set(links)];
|
links = [...new Set(links)];
|
||||||
|
|||||||
+8
-12
@@ -56,18 +56,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>PastePad</h2>
|
<h2>Tools</h2>
|
||||||
<p>Paste your clipboard or any text and manage them!</p>
|
<div
|
||||||
<div class="actions">
|
class="actions"
|
||||||
<a href="/pastepad" class="btn">Visit</a>
|
style="flex-direction: column; align-items: stretch"
|
||||||
</div>
|
>
|
||||||
</div>
|
<a href="/manage" class="btn">Queue Manager</a>
|
||||||
|
<a href="/pastepad" class="btn">PastePad</a>
|
||||||
<div class="card">
|
<a href="/extractor" class="btn">Link Extractor</a>
|
||||||
<h2>Extractor</h2>
|
|
||||||
<p>Extract links from HTML or Markdown.</p>
|
|
||||||
<div class="actions">
|
|
||||||
<a href="/extractor" class="btn">Visit</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+31
-10
@@ -31,28 +31,49 @@ queue_items %}
|
|||||||
<a href="{{ item }}" target="_blank" rel="noreferrer noopener">
|
<a href="{{ item }}" target="_blank" rel="noreferrer noopener">
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</a>
|
</a>
|
||||||
{% if not loop.index > qr_limit %}
|
|
||||||
<div class="qrcode"></div>
|
<div class="qrcode"></div>
|
||||||
{% endif %}
|
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener("load", (_) => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
document.querySelectorAll(".qrcode").forEach((e) => {
|
const observer = new IntersectionObserver(
|
||||||
// Find the sibling <a> tag within the same <li>
|
(entries, obs) => {
|
||||||
const link = e.parentElement.querySelector("a");
|
entries.forEach((entry) => {
|
||||||
const a = link ? link.textContent.trim() : "";
|
if (entry.isIntersecting) {
|
||||||
if (a) {
|
const li = entry.target;
|
||||||
new QRCode(e, {
|
const el = li.querySelector(".qrcode");
|
||||||
text: a,
|
|
||||||
|
// Avoid regenerating if already done
|
||||||
|
if (!el || el.childElementCount > 0) {
|
||||||
|
obs.unobserve(li);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = li.querySelector("a");
|
||||||
|
const url = link ? link.textContent.trim() : "";
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
try {
|
||||||
|
new QRCode(el, {
|
||||||
|
text: url,
|
||||||
correctLevel: QRCode.CorrectLevel.L,
|
correctLevel: QRCode.CorrectLevel.L,
|
||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("QR Gen failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
obs.unobserve(li);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
{ rootMargin: "200px" }
|
||||||
|
);
|
||||||
|
|
||||||
|
document.querySelectorAll(".queue-grid li").forEach((el) => observer.observe(el));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %} {% endmacro %}
|
{% endif %} {% endmacro %}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Queue Manager{% endblock %} {% block
|
||||||
|
body_class %}layout-wide{% endblock %} {% block content %} {% from "macros.html"
|
||||||
|
import qrcode_queue %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h1>Queue Manager</h1>
|
||||||
|
<div class="actions">
|
||||||
|
<button onclick="dedupQueue()" class="btn btn-sm">Dedup</button>
|
||||||
|
<button onclick="sortQueue(false)" class="btn btn-sm">
|
||||||
|
Sort A-Z
|
||||||
|
</button>
|
||||||
|
<button onclick="sortQueue(true)" class="btn btn-sm">
|
||||||
|
Sort Z-A
|
||||||
|
</button>
|
||||||
|
<button onclick="clearQueue()" class="btn btn-sm btn-danger">
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search-input"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Search links..."
|
||||||
|
style="margin-bottom: 20px"
|
||||||
|
onkeyup="filterQueue()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div id="queue-container">
|
||||||
|
{% if queue_items %}
|
||||||
|
<p id="count-display">Total: {{ queue_items|length }}</p>
|
||||||
|
{{ qrcode_queue(queue_items) }} {% else %}
|
||||||
|
<p>Queue is empty.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function dedupQueue() {
|
||||||
|
if (!confirm("Remove duplicates?")) return;
|
||||||
|
await fetch("/api/queue/dedup", { method: "POST" });
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sortQueue(reverse) {
|
||||||
|
await fetch(`/api/queue/sort?reverse=${reverse}`, { method: "POST" });
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearQueue() {
|
||||||
|
if (!confirm("Clear entire queue?")) return;
|
||||||
|
await fetch("/api/queue/clear", { method: "POST" });
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterQueue() {
|
||||||
|
const input = document.getElementById("search-input");
|
||||||
|
const filter = input.value.toLowerCase();
|
||||||
|
const listItems = document.querySelectorAll(".queue-grid li");
|
||||||
|
let visibleCount = 0;
|
||||||
|
|
||||||
|
listItems.forEach((li) => {
|
||||||
|
const link = li.querySelector("a");
|
||||||
|
const text = link.textContent || link.innerText;
|
||||||
|
if (text.toLowerCase().indexOf(filter) > -1) {
|
||||||
|
li.style.display = "";
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
li.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"count-display"
|
||||||
|
).textContent = `Showing: ${visibleCount} / ${listItems.length}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
Reference in new issue
Block a user