This commit is contained in:
lda committed 2025-12-26 01:16:48 +07:00
1 parent 1911d55d5e
commit 58d3e58759
7 files changed
+274 -95

No files matched your search

+94 -55
View File
@@ -14,6 +14,7 @@ body_class %}layout-wide{% endblock %}{% block content %}
id="file-upload"
style="display: none"
accept=".html,.htm,.md,.txt,.markdown"
multiple
/>
<span
id="file-name"
@@ -34,6 +35,9 @@ body_class %}layout-wide{% endblock %}{% block content %}
<button onclick="extractLinks('markdown')" class="btn">
Extract from Markdown
</button>
<button onclick="extractLinks('jumblang')" class="btn">
Extract from Messy Text
</button>
<button onclick="extractLinks('auto')" class="btn btn-primary">
Auto Detect & Extract
</button>
@@ -81,41 +85,61 @@ body_class %}layout-wide{% endblock %}{% block content %}
// File Upload Handler
/** @type {HTMLInputElement} */
const fileInput = document.getElementById("file-upload");
fileInput.addEventListener("change", function (e) {
/** @type {File} */
const file = e.target.files[0];
if (!file) return;
document.getElementById("file-name").textContent = file.name;
fileInput.addEventListener("change", async function (e) {
/** @type {FileList} */
const files = e.target.files;
if (!files || files.length === 0) return;
document.getElementById(
"file-name"
).textContent = `${files.length} file(s) selected`;
/** @type {HTMLTextAreaElement} */
const textarea = document.getElementById("input-content");
textarea.value = ""; // Clear previous content
// 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 (
firstFile.name.endsWith(".md") ||
firstFile.name.endsWith(".markdown")
) {
extractLinks("markdown");
} else if (
firstFile.name.endsWith(".html") ||
firstFile.name.endsWith(".htm")
) {
extractLinks("html");
}
});
/**
* Reads a file as text using a Promise wrapper around FileReader.
* @param {File} file
* @returns {Promise<string>}
*/
function readFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function (e) {
/** @type {HTMLTextAreaElement} */
const textarea = document.getElementById("input-content");
textarea.value = e.target.result;
// Optional: Auto-detect mode based on extension
if (
file.name.endsWith(".md") ||
file.name.endsWith(".markdown")
) {
extractLinks("markdown");
} else if (
file.name.endsWith(".html") ||
file.name.endsWith(".htm")
) {
extractLinks("html");
}
};
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'} mode
* @param {'html' | 'markdown' | 'auto' | 'jumblang'} mode
*/
function extractLinks(mode) {
async function extractLinks(mode) {
/** @type {string} */
const content = document.getElementById("input-content").value;
if (!content.trim()) return;
@@ -123,38 +147,53 @@ body_class %}layout-wide{% endblock %}{% block content %}
/** @type {string[]} */
let links = [];
if (mode === "auto") {
// Simple heuristic: if it has <a href, it's likely HTML.
mode =
content.includes("<a href") ||
content.includes("<!DOCTYPE html>")
? "html"
: "markdown";
}
if (mode === "html") {
const parser = new DOMParser();
const doc = parser.parseFromString(content, "text/html");
const anchors = doc.querySelectorAll("a[href]");
links = Array.from(anchors)
.map((a) => a.href)
.filter((h) => h.startsWith("http"));
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 {
// Markdown regex for [text](url) and <url>
const mdLinkRegex = /\[.*?\]\((https?:\/\/[^\s)]+)\)/g;
const rawLinkRegex = /<(https?:\/\/[^>]+)>/g;
let match;
while ((match = mdLinkRegex.exec(content)) !== null) {
links.push(match[1]);
}
while ((match = rawLinkRegex.exec(content)) !== null) {
links.push(match[1]);
if (mode === "auto") {
// Simple heuristic: if it has <a href, it's likely HTML.
mode =
content.includes("<a href") ||
content.includes("<!DOCTYPE html>")
? "html"
: "markdown";
}
// Also catch plain URLs if they are on their own line or space-separated
// 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.
if (mode === "html") {
const parser = new DOMParser();
const doc = parser.parseFromString(content, "text/html");
const anchors = doc.querySelectorAll("a[href]");
links = Array.from(anchors)
.map((a) => a.href)
.filter((h) => h.startsWith("http"));
} else {
// Markdown regex for [text](url) and <url>
const mdLinkRegex = /\[.*?\]\((https?:\/\/[^\s)]+)\)/g;
const rawLinkRegex = /<(https?:\/\/[^>]+)>/g;
let match;
while ((match = mdLinkRegex.exec(content)) !== null) {
links.push(match[1]);
}
while ((match = rawLinkRegex.exec(content)) !== null) {
links.push(match[1]);
}
// Also catch plain URLs if they are on their own line or space-separated
// 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.
}
}
// Deduplicate
+8 -12
View File
@@ -56,18 +56,14 @@
</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 class="card">
<h2>Extractor</h2>
<p>Extract links from HTML or Markdown.</p>
<div class="actions">
<a href="/extractor" class="btn">Visit</a>
<h2>Tools</h2>
<div
class="actions"
style="flex-direction: column; align-items: stretch"
>
<a href="/manage" class="btn">Queue Manager</a>
<a href="/pastepad" class="btn">PastePad</a>
<a href="/extractor" class="btn">Link Extractor</a>
</div>
</div>
</div>
+36 -15
View File
@@ -31,28 +31,49 @@ queue_items %}
<a href="{{ item }}" target="_blank" rel="noreferrer noopener">
{{ item }}
</a>
{% if not loop.index > qr_limit %}
<div class="qrcode"></div>
{% endif %}
</li>
{% endfor %}
</ol>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js"></script>
<script>
window.addEventListener("load", (_) => {
document.querySelectorAll(".qrcode").forEach((e) => {
// 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,
document.addEventListener("DOMContentLoaded", () => {
const observer = new IntersectionObserver(
(entries, obs) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const li = entry.target;
const el = li.querySelector(".qrcode");
// 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,
width: 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>
{% endif %} {% endmacro %}
+88
View File
@@ -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 %}
-6
View File
@@ -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 %}