mirror of
https://codeberg.org/listyantidewi/your-everyday-tools.git
synced 2026-07-01 23:17:37 +08:00
feature updates
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Your Everyday Tools
|
||||
|
||||
A lightweight, self-hosted web app that bundles 33 everyday utilities into a single interface. Built with Python + Flask, zero JavaScript frameworks, and minimal CSS — no bloat, just tools.
|
||||
A lightweight, self-hosted web app that bundles 48 everyday utilities into a single interface. Built with Python + Flask, zero JavaScript frameworks, and minimal CSS — no bloat, just tools.
|
||||
|
||||

|
||||

|
||||
@@ -17,6 +17,7 @@ A lightweight, self-hosted web app that bundles 33 everyday utilities into a sin
|
||||
| **PDF to Word** | Convert PDF documents to `.docx` format |
|
||||
| **PDF to Images** | Export each PDF page as PNG or JPG (configurable DPI) |
|
||||
| **PDF to Text** | Extract all text content from a PDF |
|
||||
| **HTML to PDF** | Convert HTML content to a PDF document |
|
||||
|
||||
### PDF Tools
|
||||
| Tool | Description |
|
||||
@@ -41,6 +42,9 @@ A lightweight, self-hosted web app that bundles 33 everyday utilities into a sin
|
||||
| **Crop Image** | Crop by aspect ratio (1:1, 4:3, 16:9, etc.) or custom coordinates |
|
||||
| **Rotate / Flip** | Rotate 90/180/270 degrees, flip horizontal or vertical |
|
||||
| **Add Watermark** | Add text watermark with configurable position, opacity, size, and tiled mode |
|
||||
| **EXIF Viewer** | View or strip image metadata (EXIF data) for privacy |
|
||||
| **Favicon Generator** | Create .ico favicons from any image with multiple size options |
|
||||
| **Image to Text (OCR)** | Extract text from images using optical character recognition |
|
||||
|
||||
### Text & Data (client-side, no upload needed)
|
||||
| Tool | Description |
|
||||
@@ -51,6 +55,12 @@ A lightweight, self-hosted web app that bundles 33 everyday utilities into a sin
|
||||
| **URL Encode** | Encode and decode URL components |
|
||||
| **Word Counter** | Count words, characters, sentences, paragraphs, and estimate reading time |
|
||||
| **Markdown Preview** | Live Markdown-to-HTML preview |
|
||||
| **Case Converter** | Convert between UPPER, lower, Title, camelCase, snake_case, kebab-case, PascalCase |
|
||||
| **Text Diff** | Compare two texts side by side with highlighted additions and deletions |
|
||||
| **Regex Tester** | Test regular expressions with live match highlighting and group extraction |
|
||||
| **Slug Generator** | Create URL-friendly slugs from any text |
|
||||
| **JSON / YAML** | Convert between JSON and YAML formats |
|
||||
| **Lorem Ipsum** | Generate placeholder text by paragraphs, sentences, or words |
|
||||
|
||||
### Calculators (client-side)
|
||||
| Tool | Description |
|
||||
@@ -60,6 +70,9 @@ A lightweight, self-hosted web app that bundles 33 everyday utilities into a sin
|
||||
| **Color Converter** | Convert between HEX, RGB, and HSL with live preview and color picker |
|
||||
| **Percentage Calc** | Four common percentage calculations in one page |
|
||||
| **Date Calculator** | Date difference, add/subtract days, day-of-week lookup |
|
||||
| **Timestamp Converter** | Convert between Unix timestamps and human-readable dates (local, UTC, ISO 8601) |
|
||||
| **Number Base Converter** | Convert between decimal, binary, octal, and hexadecimal |
|
||||
| **Pomodoro Timer** | Focus timer with configurable work/break intervals and session tracking |
|
||||
|
||||
### QR Code
|
||||
| Tool | Description |
|
||||
@@ -67,6 +80,12 @@ A lightweight, self-hosted web app that bundles 33 everyday utilities into a sin
|
||||
| **Generate QR** | Create QR codes from text/URLs with custom size, border, and color |
|
||||
| **Read QR** | Decode QR codes from uploaded images |
|
||||
|
||||
### Security
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| **Password Generator** | Generate strong random passwords with configurable length, character types, and entropy display |
|
||||
| **Hash Generator** | Generate MD5, SHA-1, SHA-256, and SHA-512 hashes from text |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
@@ -110,11 +129,12 @@ The core app works out of the box with the main dependencies. Some features requ
|
||||
| `rembg` | Remove Background | Installs ONNX Runtime (~500 MB). The app works without it and shows a helpful message if missing. |
|
||||
| `pyzbar` | Read QR Code | Requires the [ZBar](https://github.com/NaturalHistoryMuseum/pyzbar#installation) shared library on your system. |
|
||||
| `pdf2docx` | PDF to Word | Pure Python, but conversion quality depends on PDF complexity. |
|
||||
| `pytesseract` | Image to Text (OCR) | Requires the [Tesseract](https://github.com/tesseract-ocr/tesseract) binary installed on your system. |
|
||||
|
||||
If you only need the core tools, install the minimal set:
|
||||
|
||||
```bash
|
||||
pip install Flask Pillow PyMuPDF "qrcode[pil]" markdown reportlab img2pdf
|
||||
pip install Flask Pillow PyMuPDF "qrcode[pil]" markdown reportlab img2pdf python-docx
|
||||
```
|
||||
|
||||
---
|
||||
@@ -133,7 +153,8 @@ your-everyday-tools/
|
||||
│ ├── image_tools.py # Image processing endpoints
|
||||
│ ├── text_tools.py # Text & data tool page routes
|
||||
│ ├── calculator_tools.py # Calculator page routes
|
||||
│ └── qr_tools.py # QR code endpoints
|
||||
│ ├── qr_tools.py # QR code endpoints
|
||||
│ └── security_tools.py # Security tool page routes
|
||||
├── templates/
|
||||
│ ├── base.html # Main layout (sidebar + content area)
|
||||
│ ├── index.html # Home page with tool cards
|
||||
@@ -144,12 +165,23 @@ your-everyday-tools/
|
||||
│ ├── color_converter.html
|
||||
│ ├── percentage_calc.html
|
||||
│ ├── date_calc.html
|
||||
│ ├── timestamp_converter.html
|
||||
│ ├── number_base.html
|
||||
│ ├── pomodoro.html
|
||||
│ ├── json_formatter.html
|
||||
│ ├── csv_json.html
|
||||
│ ├── json_yaml.html
|
||||
│ ├── base64.html
|
||||
│ ├── url_encode.html
|
||||
│ ├── word_counter.html
|
||||
│ └── markdown_preview.html
|
||||
│ ├── markdown_preview.html
|
||||
│ ├── case_converter.html
|
||||
│ ├── text_diff.html
|
||||
│ ├── regex_tester.html
|
||||
│ ├── slug_generator.html
|
||||
│ ├── lorem_ipsum.html
|
||||
│ ├── password_generator.html
|
||||
│ └── hash_generator.html
|
||||
└── static/
|
||||
├── css/style.css # All styles (~400 lines, no framework)
|
||||
└── js/main.js # File upload, AJAX, sidebar, shared logic
|
||||
@@ -157,11 +189,11 @@ your-everyday-tools/
|
||||
|
||||
### Architecture Notes
|
||||
|
||||
- **One universal template** — `upload_tool.html` powers all 20+ server-side tools. Each route passes title, description, accepted file types, and form options as template variables. No per-tool template duplication.
|
||||
- **Client-side tools** (text utilities, calculators) run entirely in the browser with vanilla JavaScript — zero server round-trips.
|
||||
- **One universal template** — `upload_tool.html` powers all 25+ server-side tools. Each route passes title, description, accepted file types, and form options as template variables. No per-tool template duplication.
|
||||
- **Client-side tools** (text utilities, calculators, security tools) run entirely in the browser with vanilla JavaScript — zero server round-trips.
|
||||
- **In-memory processing** — all file operations use `BytesIO`. No temporary files are written to disk.
|
||||
- **No CSS framework** — custom CSS with CSS Grid, Flexbox, and CSS custom properties. The only external resource is Bootstrap Icons via CDN (~100 KB) for the icon set.
|
||||
- **Graceful degradation** — heavy optional packages (`rembg`, `pyzbar`, `pdf2docx`) are checked at import time. If missing, the affected tool shows a clear install instruction instead of crashing.
|
||||
- **Graceful degradation** — heavy optional packages (`rembg`, `pyzbar`, `pdf2docx`, `pytesseract`) are checked at import time. If missing, the affected tool shows a clear install instruction instead of crashing.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+5
-1
@@ -1,3 +1,4 @@
|
||||
# Core
|
||||
Flask
|
||||
Pillow
|
||||
PyMuPDF
|
||||
@@ -5,7 +6,10 @@ qrcode[pil]
|
||||
markdown
|
||||
reportlab
|
||||
img2pdf
|
||||
python-docx
|
||||
|
||||
# Optional (app works without these, shows install message if missing)
|
||||
rembg
|
||||
pyzbar
|
||||
pdf2docx
|
||||
python-docx
|
||||
pytesseract
|
||||
|
||||
@@ -330,3 +330,30 @@ def pdf_to_text():
|
||||
|
||||
doc.close()
|
||||
return jsonify(text="\n".join(text_parts))
|
||||
|
||||
|
||||
@bp.route("/html-to-pdf", methods=["POST"])
|
||||
def html_to_pdf():
|
||||
html = request.form.get("text", "").strip()
|
||||
if not html:
|
||||
return jsonify(error="Please enter some HTML content."), 400
|
||||
|
||||
doc = fitz.open()
|
||||
page = doc.new_page(width=595, height=842) # A4
|
||||
|
||||
# Wrap in basic structure if no <html> tag present
|
||||
if "<html" not in html.lower():
|
||||
html = f"<html><body>{html}</body></html>"
|
||||
|
||||
try:
|
||||
page.insert_htmlbox(fitz.Rect(50, 50, 545, 792), html)
|
||||
except Exception as e:
|
||||
return jsonify(error=f"HTML rendering failed: {str(e)}"), 400
|
||||
|
||||
output = io.BytesIO()
|
||||
doc.save(output)
|
||||
doc.close()
|
||||
output.seek(0)
|
||||
|
||||
return send_file(output, mimetype="application/pdf",
|
||||
as_attachment=True, download_name="converted.pdf")
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Case Converter - EveryTools{% endblock %}
|
||||
{% block top_title %}Case Converter{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool">
|
||||
<div class="tool-header">
|
||||
<h1>Case Converter</h1>
|
||||
<p>Convert text between different cases</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Input Text</label>
|
||||
<textarea id="case-input" class="text-input" rows="4" placeholder="Type or paste your text here..." oninput="convertAll()"></textarea>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.8rem">
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .8rem;padding:.5rem .8rem"><span>UPPERCASE</span>
|
||||
<button class="btn btn-small" onclick="copyText('case-upper')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<div id="case-upper" style="font-size:.9rem;word-break:break-word;min-height:2em"></div>
|
||||
</div>
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .8rem;padding:.5rem .8rem"><span>lowercase</span>
|
||||
<button class="btn btn-small" onclick="copyText('case-lower')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<div id="case-lower" style="font-size:.9rem;word-break:break-word;min-height:2em"></div>
|
||||
</div>
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .8rem;padding:.5rem .8rem"><span>Title Case</span>
|
||||
<button class="btn btn-small" onclick="copyText('case-title')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<div id="case-title" style="font-size:.9rem;word-break:break-word;min-height:2em"></div>
|
||||
</div>
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .8rem;padding:.5rem .8rem"><span>Sentence case</span>
|
||||
<button class="btn btn-small" onclick="copyText('case-sentence')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<div id="case-sentence" style="font-size:.9rem;word-break:break-word;min-height:2em"></div>
|
||||
</div>
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .8rem;padding:.5rem .8rem"><span>camelCase</span>
|
||||
<button class="btn btn-small" onclick="copyText('case-camel')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<div id="case-camel" style="font-size:.9rem;word-break:break-word;min-height:2em"></div>
|
||||
</div>
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .8rem;padding:.5rem .8rem"><span>snake_case</span>
|
||||
<button class="btn btn-small" onclick="copyText('case-snake')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<div id="case-snake" style="font-size:.9rem;word-break:break-word;min-height:2em"></div>
|
||||
</div>
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .8rem;padding:.5rem .8rem"><span>kebab-case</span>
|
||||
<button class="btn btn-small" onclick="copyText('case-kebab')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<div id="case-kebab" style="font-size:.9rem;word-break:break-word;min-height:2em"></div>
|
||||
</div>
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .8rem;padding:.5rem .8rem"><span>PascalCase</span>
|
||||
<button class="btn btn-small" onclick="copyText('case-pascal')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<div id="case-pascal" style="font-size:.9rem;word-break:break-word;min-height:2em"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function copyText(id) { navigator.clipboard.writeText(document.getElementById(id).textContent); }
|
||||
|
||||
function toWords(s) {
|
||||
return s.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.replace(/[_\-]+/g, ' ')
|
||||
.trim().split(/\s+/).filter(w => w);
|
||||
}
|
||||
|
||||
function convertAll() {
|
||||
const t = document.getElementById("case-input").value;
|
||||
const words = toWords(t);
|
||||
document.getElementById("case-upper").textContent = t.toUpperCase();
|
||||
document.getElementById("case-lower").textContent = t.toLowerCase();
|
||||
document.getElementById("case-title").textContent = words.map(w => w[0].toUpperCase() + w.slice(1).toLowerCase()).join(" ");
|
||||
document.getElementById("case-sentence").textContent = t.charAt(0).toUpperCase() + t.slice(1).toLowerCase();
|
||||
document.getElementById("case-camel").textContent = words.map((w, i) => i === 0 ? w.toLowerCase() : w[0].toUpperCase() + w.slice(1).toLowerCase()).join("");
|
||||
document.getElementById("case-snake").textContent = words.map(w => w.toLowerCase()).join("_");
|
||||
document.getElementById("case-kebab").textContent = words.map(w => w.toLowerCase()).join("-");
|
||||
document.getElementById("case-pascal").textContent = words.map(w => w[0].toUpperCase() + w.slice(1).toLowerCase()).join("");
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,153 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Hash Generator - EveryTools{% endblock %}
|
||||
{% block top_title %}Hash Generator{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool">
|
||||
<div class="tool-header">
|
||||
<h1>Hash Generator</h1>
|
||||
<p>Generate MD5, SHA-1, SHA-256, and SHA-512 hashes from text</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Input Text</label>
|
||||
<textarea id="hash-input" class="text-input" rows="4" placeholder="Enter text to hash..." oninput="computeHashes()"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="hash-results" style="display:grid;gap:.8rem">
|
||||
<div class="calc-section" id="hash-md5-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .5rem;padding:.4rem .8rem">
|
||||
<span>MD5</span>
|
||||
<button class="btn btn-small" onclick="copyHash('hash-md5')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<code id="hash-md5" style="word-break:break-all;font-size:.85rem;color:var(--text-light)">-</code>
|
||||
</div>
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .5rem;padding:.4rem .8rem">
|
||||
<span>SHA-1</span>
|
||||
<button class="btn btn-small" onclick="copyHash('hash-sha1')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<code id="hash-sha1" style="word-break:break-all;font-size:.85rem;color:var(--text-light)">-</code>
|
||||
</div>
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .5rem;padding:.4rem .8rem">
|
||||
<span>SHA-256</span>
|
||||
<button class="btn btn-small" onclick="copyHash('hash-sha256')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<code id="hash-sha256" style="word-break:break-all;font-size:.85rem;color:var(--text-light)">-</code>
|
||||
</div>
|
||||
<div class="calc-section">
|
||||
<div class="pane-header" style="margin:-1.2rem -1.2rem .5rem;padding:.4rem .8rem">
|
||||
<span>SHA-512</span>
|
||||
<button class="btn btn-small" onclick="copyHash('hash-sha512')"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<code id="hash-sha512" style="word-break:break-all;font-size:.85rem;color:var(--text-light)">-</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function computeHashes() {
|
||||
const text = document.getElementById("hash-input").value;
|
||||
if (!text) {
|
||||
["md5","sha1","sha256","sha512"].forEach(h => document.getElementById("hash-" + h).textContent = "-");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new TextEncoder().encode(text);
|
||||
|
||||
// Web Crypto API for SHA family
|
||||
const sha1 = await digest("SHA-1", data);
|
||||
const sha256 = await digest("SHA-256", data);
|
||||
const sha512 = await digest("SHA-512", data);
|
||||
|
||||
// MD5 via pure JS (Web Crypto doesn't support it)
|
||||
const md5 = md5Hash(text);
|
||||
|
||||
document.getElementById("hash-md5").textContent = md5;
|
||||
document.getElementById("hash-sha1").textContent = sha1;
|
||||
document.getElementById("hash-sha256").textContent = sha256;
|
||||
document.getElementById("hash-sha512").textContent = sha512;
|
||||
}
|
||||
|
||||
async function digest(algo, data) {
|
||||
const buf = await crypto.subtle.digest(algo, data);
|
||||
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function copyHash(id) {
|
||||
const text = document.getElementById(id).textContent;
|
||||
if (text !== "-") navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
// MD5 implementation (RFC 1321)
|
||||
function md5Hash(string) {
|
||||
function md5cycle(x, k) {
|
||||
let a = x[0], b = x[1], c = x[2], d = x[3];
|
||||
a = ff(a,b,c,d,k[0],7,-680876936); d = ff(d,a,b,c,k[1],12,-389564586);
|
||||
c = ff(c,d,a,b,k[2],17,606105819); b = ff(b,c,d,a,k[3],22,-1044525330);
|
||||
a = ff(a,b,c,d,k[4],7,-176418897); d = ff(d,a,b,c,k[5],12,1200080426);
|
||||
c = ff(c,d,a,b,k[6],17,-1473231341); b = ff(b,c,d,a,k[7],22,-45705983);
|
||||
a = ff(a,b,c,d,k[8],7,1770035416); d = ff(d,a,b,c,k[9],12,-1958414417);
|
||||
c = ff(c,d,a,b,k[10],17,-42063); b = ff(b,c,d,a,k[11],22,-1990404162);
|
||||
a = ff(a,b,c,d,k[12],7,1804603682); d = ff(d,a,b,c,k[13],12,-40341101);
|
||||
c = ff(c,d,a,b,k[14],17,-1502002290); b = ff(b,c,d,a,k[15],22,1236535329);
|
||||
a = gg(a,b,c,d,k[1],5,-165796510); d = gg(d,a,b,c,k[6],9,-1069501632);
|
||||
c = gg(c,d,a,b,k[11],14,643717713); b = gg(b,c,d,a,k[0],20,-373897302);
|
||||
a = gg(a,b,c,d,k[5],5,-701558691); d = gg(d,a,b,c,k[10],9,38016083);
|
||||
c = gg(c,d,a,b,k[15],14,-660478335); b = gg(b,c,d,a,k[4],20,-405537848);
|
||||
a = gg(a,b,c,d,k[9],5,568446438); d = gg(d,a,b,c,k[14],9,-1019803690);
|
||||
c = gg(c,d,a,b,k[3],14,-187363961); b = gg(b,c,d,a,k[8],20,1163531501);
|
||||
a = gg(a,b,c,d,k[13],5,-1444681467); d = gg(d,a,b,c,k[2],9,-51403784);
|
||||
c = gg(c,d,a,b,k[7],14,1735328473); b = gg(b,c,d,a,k[12],20,-1926607734);
|
||||
a = hh(a,b,c,d,k[5],4,-378558); d = hh(d,a,b,c,k[8],11,-2022574463);
|
||||
c = hh(c,d,a,b,k[11],16,1839030562); b = hh(b,c,d,a,k[14],23,-35309556);
|
||||
a = hh(a,b,c,d,k[1],4,-1530992060); d = hh(d,a,b,c,k[4],11,1272893353);
|
||||
c = hh(c,d,a,b,k[7],16,-155497632); b = hh(b,c,d,a,k[10],23,-1094730640);
|
||||
a = hh(a,b,c,d,k[13],4,681279174); d = hh(d,a,b,c,k[0],11,-358537222);
|
||||
c = hh(c,d,a,b,k[3],16,-722521979); b = hh(b,c,d,a,k[6],23,76029189);
|
||||
a = hh(a,b,c,d,k[9],4,-640364487); d = hh(d,a,b,c,k[12],11,-421815835);
|
||||
c = hh(c,d,a,b,k[15],16,530742520); b = hh(b,c,d,a,k[2],23,-995338651);
|
||||
a = ii(a,b,c,d,k[0],6,-198630844); d = ii(d,a,b,c,k[7],10,1126891415);
|
||||
c = ii(c,d,a,b,k[14],15,-1416354905); b = ii(b,c,d,a,k[5],21,-57434055);
|
||||
a = ii(a,b,c,d,k[12],6,1700485571); d = ii(d,a,b,c,k[3],10,-1894986606);
|
||||
c = ii(c,d,a,b,k[10],15,-1051523); b = ii(b,c,d,a,k[1],21,-2054922799);
|
||||
a = ii(a,b,c,d,k[8],6,1873313359); d = ii(d,a,b,c,k[15],10,-30611744);
|
||||
c = ii(c,d,a,b,k[6],15,-1560198380); b = ii(b,c,d,a,k[13],21,1309151649);
|
||||
a = ii(a,b,c,d,k[4],6,-145523070); d = ii(d,a,b,c,k[11],10,-1120210379);
|
||||
c = ii(c,d,a,b,k[2],15,718787259); b = ii(b,c,d,a,k[9],21,-343485551);
|
||||
x[0] = add32(a,x[0]); x[1] = add32(b,x[1]); x[2] = add32(c,x[2]); x[3] = add32(d,x[3]);
|
||||
}
|
||||
function cmn(q,a,b,x,s,t) { a = add32(add32(a,q),add32(x,t)); return add32((a<<s)|(a>>>(32-s)),b); }
|
||||
function ff(a,b,c,d,x,s,t) { return cmn((b&c)|((~b)&d),a,b,x,s,t); }
|
||||
function gg(a,b,c,d,x,s,t) { return cmn((b&d)|(c&(~d)),a,b,x,s,t); }
|
||||
function hh(a,b,c,d,x,s,t) { return cmn(b^c^d,a,b,x,s,t); }
|
||||
function ii(a,b,c,d,x,s,t) { return cmn(c^(b|(~d)),a,b,x,s,t); }
|
||||
function md51(s) {
|
||||
const n = s.length;
|
||||
let state = [1732584193,-271733879,-1732584194,271733878], i;
|
||||
for (i=64; i<=n; i+=64) md5cycle(state, md5blk(s.substring(i-64,i)));
|
||||
s = s.substring(i-64);
|
||||
const tail = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
|
||||
for (i=0; i<s.length; i++) tail[i>>2] |= s.charCodeAt(i)<<((i%4)<<3);
|
||||
tail[i>>2] |= 0x80<<((i%4)<<3);
|
||||
if (i>55) { md5cycle(state,tail); for (i=0; i<16; i++) tail[i]=0; }
|
||||
tail[14] = n*8;
|
||||
md5cycle(state, tail);
|
||||
return state;
|
||||
}
|
||||
function md5blk(s) {
|
||||
const md5blks = []; for (let i=0; i<64; i+=4) md5blks[i>>2] = s.charCodeAt(i)+(s.charCodeAt(i+1)<<8)+(s.charCodeAt(i+2)<<16)+(s.charCodeAt(i+3)<<24);
|
||||
return md5blks;
|
||||
}
|
||||
function add32(a,b) { return (a+b) & 0xFFFFFFFF; }
|
||||
function rhex(n) { let s=""; for (let j=0; j<4; j++) s += "0123456789abcdef".charAt((n>>(j*8+4))&0x0F) + "0123456789abcdef".charAt((n>>(j*8))&0x0F); return s; }
|
||||
function hex(x) { return x.map(rhex).join(""); }
|
||||
// Encode as UTF-8 bytes for correct hashing
|
||||
const utf8 = unescape(encodeURIComponent(string));
|
||||
return hex(md51(utf8));
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,190 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}JSON / YAML Converter - EveryTools{% endblock %}
|
||||
{% block top_title %}JSON / YAML{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool">
|
||||
<div class="tool-header">
|
||||
<h1>JSON / YAML Converter</h1>
|
||||
<p>Convert between JSON and YAML formats</p>
|
||||
</div>
|
||||
<div style="margin-bottom:1rem">
|
||||
<button class="btn btn-primary" id="btn-j2y" onclick="setMode('j2y')" style="opacity:1">JSON → YAML</button>
|
||||
<button class="btn btn-primary" id="btn-y2j" onclick="setMode('y2j')" style="opacity:.5">YAML → JSON</button>
|
||||
</div>
|
||||
<div class="split-pane">
|
||||
<div class="pane">
|
||||
<div class="pane-header"><span id="jy-input-label">JSON Input</span></div>
|
||||
<div class="pane-body">
|
||||
<textarea id="jy-input" placeholder="Paste your data here..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<div class="pane-header">
|
||||
<span id="jy-output-label">YAML Output</span>
|
||||
<button class="btn btn-small" onclick="navigator.clipboard.writeText(document.getElementById('jy-output').textContent)"><i class="bi bi-clipboard"></i> Copy</button>
|
||||
</div>
|
||||
<div class="pane-body">
|
||||
<pre id="jy-output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" style="margin-top:1rem" onclick="convert()">Convert</button>
|
||||
<div id="jy-status" style="margin-top:.5rem;font-size:.85rem"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let mode = "j2y";
|
||||
function setMode(m) {
|
||||
mode = m;
|
||||
document.getElementById("btn-j2y").style.opacity = m === "j2y" ? 1 : .5;
|
||||
document.getElementById("btn-y2j").style.opacity = m === "y2j" ? 1 : .5;
|
||||
document.getElementById("jy-input-label").textContent = m === "j2y" ? "JSON Input" : "YAML Input";
|
||||
document.getElementById("jy-output-label").textContent = m === "j2y" ? "YAML Output" : "JSON Output";
|
||||
}
|
||||
|
||||
function convert() {
|
||||
const input = document.getElementById("jy-input").value.trim();
|
||||
const status = document.getElementById("jy-status");
|
||||
const output = document.getElementById("jy-output");
|
||||
if (!input) { status.innerHTML = '<span style="color:var(--danger)">Please enter data.</span>'; return; }
|
||||
try {
|
||||
if (mode === "j2y") {
|
||||
const obj = JSON.parse(input);
|
||||
output.textContent = jsonToYaml(obj, 0);
|
||||
} else {
|
||||
const obj = yamlToJson(input);
|
||||
output.textContent = JSON.stringify(obj, null, 2);
|
||||
}
|
||||
status.innerHTML = '<span style="color:var(--success)"><i class="bi bi-check-circle"></i> Converted</span>';
|
||||
} catch (e) {
|
||||
status.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle"></i> ${e.message}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function jsonToYaml(obj, indent) {
|
||||
const pad = " ".repeat(indent);
|
||||
if (obj === null) return "null";
|
||||
if (typeof obj === "boolean") return obj ? "true" : "false";
|
||||
if (typeof obj === "number") return String(obj);
|
||||
if (typeof obj === "string") {
|
||||
if (obj.includes("\n") || obj.includes(": ") || obj.includes("#") || /^[\[\]{}>|*&!%@`,]/.test(obj))
|
||||
return JSON.stringify(obj);
|
||||
return obj;
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length === 0) return "[]";
|
||||
return obj.map(item => {
|
||||
const val = jsonToYaml(item, indent + 1);
|
||||
if (typeof item === "object" && item !== null) return pad + "- " + val.trimStart();
|
||||
return pad + "- " + val;
|
||||
}).join("\n");
|
||||
}
|
||||
const keys = Object.keys(obj);
|
||||
if (keys.length === 0) return "{}";
|
||||
return keys.map(key => {
|
||||
const val = obj[key];
|
||||
const yamlVal = jsonToYaml(val, indent + 1);
|
||||
if (typeof val === "object" && val !== null && !Array.isArray(val) && Object.keys(val).length > 0)
|
||||
return pad + key + ":\n" + yamlVal;
|
||||
if (Array.isArray(val) && val.length > 0)
|
||||
return pad + key + ":\n" + yamlVal;
|
||||
return pad + key + ": " + yamlVal;
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function yamlToJson(yaml) {
|
||||
const lines = yaml.split("\n");
|
||||
return parseYamlLines(lines, 0).value;
|
||||
}
|
||||
|
||||
function parseYamlLines(lines, startIndent) {
|
||||
const result = {};
|
||||
let isArray = false;
|
||||
const arr = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
if (line.trim() === "" || line.trim().startsWith("#")) { i++; continue; }
|
||||
|
||||
const indent = line.search(/\S/);
|
||||
if (indent < startIndent) break;
|
||||
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.startsWith("- ")) {
|
||||
isArray = true;
|
||||
const val = trimmed.slice(2).trim();
|
||||
if (val.includes(": ")) {
|
||||
// inline object in array
|
||||
const colonIdx = val.indexOf(": ");
|
||||
const k = val.slice(0, colonIdx);
|
||||
const v = parseYamlValue(val.slice(colonIdx + 2));
|
||||
arr.push({[k]: v});
|
||||
} else {
|
||||
arr.push(parseYamlValue(val));
|
||||
}
|
||||
i++;
|
||||
} else if (trimmed.includes(": ")) {
|
||||
const colonIdx = trimmed.indexOf(": ");
|
||||
const key = trimmed.slice(0, colonIdx);
|
||||
const val = trimmed.slice(colonIdx + 2).trim();
|
||||
if (val === "" || val === "|" || val === ">") {
|
||||
// nested object or block
|
||||
const nested = [];
|
||||
i++;
|
||||
while (i < lines.length) {
|
||||
const nl = lines[i];
|
||||
if (nl.trim() === "" || nl.search(/\S/) > indent) { nested.push(lines[i]); i++; }
|
||||
else break;
|
||||
}
|
||||
if (nested.length > 0 && nested[0].trim().startsWith("- ")) {
|
||||
result[key] = parseYamlLines(nested, nested[0].search(/\S/)).value;
|
||||
} else if (nested.length > 0) {
|
||||
const r = parseYamlLines(nested, nested[0].search(/\S/));
|
||||
result[key] = r.value;
|
||||
} else {
|
||||
result[key] = null;
|
||||
}
|
||||
} else {
|
||||
result[key] = parseYamlValue(val);
|
||||
i++;
|
||||
}
|
||||
} else if (trimmed.endsWith(":")) {
|
||||
const key = trimmed.slice(0, -1);
|
||||
const nested = [];
|
||||
i++;
|
||||
while (i < lines.length) {
|
||||
const nl = lines[i];
|
||||
if (nl.trim() === "" || nl.search(/\S/) > indent) { nested.push(lines[i]); i++; }
|
||||
else break;
|
||||
}
|
||||
if (nested.length > 0) {
|
||||
result[key] = parseYamlLines(nested, nested[0].search(/\S/)).value;
|
||||
} else {
|
||||
result[key] = null;
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return { value: isArray ? arr : result };
|
||||
}
|
||||
|
||||
function parseYamlValue(s) {
|
||||
if (s === "null" || s === "~") return null;
|
||||
if (s === "true") return true;
|
||||
if (s === "false") return false;
|
||||
if (s === "[]") return [];
|
||||
if (s === "{}") return {};
|
||||
if (/^-?\d+$/.test(s)) return parseInt(s);
|
||||
if (/^-?\d+\.\d+$/.test(s)) return parseFloat(s);
|
||||
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'")))
|
||||
return s.slice(1, -1);
|
||||
return s;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,92 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Lorem Ipsum Generator - EveryTools{% endblock %}
|
||||
{% block top_title %}Lorem Ipsum{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool">
|
||||
<div class="tool-header">
|
||||
<h1>Lorem Ipsum Generator</h1>
|
||||
<p>Generate placeholder text for your designs and layouts</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:.8rem;align-items:flex-end;margin-bottom:1rem;flex-wrap:wrap">
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label>Amount</label>
|
||||
<input type="number" id="li-count" value="3" min="1" max="100" style="width:80px" oninput="generate()">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label>Type</label>
|
||||
<select id="li-type" onchange="generate()">
|
||||
<option value="paragraphs">Paragraphs</option>
|
||||
<option value="sentences">Sentences</option>
|
||||
<option value="words">Words</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="checkbox-label" style="margin-bottom:.3rem">
|
||||
<input type="checkbox" id="li-start" checked onchange="generate()">
|
||||
<span>Start with "Lorem ipsum..."</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<div class="pane-header">
|
||||
<span>Generated Text</span>
|
||||
<button class="btn btn-small" onclick="navigator.clipboard.writeText(document.getElementById('li-output').textContent)"><i class="bi bi-clipboard"></i> Copy</button>
|
||||
</div>
|
||||
<div class="pane-body">
|
||||
<div id="li-output" style="white-space:pre-wrap;line-height:1.7;font-size:.9rem;min-height:150px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
|
||||
|
||||
const WORDS = "a ac accumsan adipiscing aenean aliquam aliquet amet ante aptent arcu at auctor augue bibendum blandit class commodo condimentum congue consectetur consequat conubia convallis cras cubilia cum curabitur cursus dapibus diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac hendrerit himenaeos iaculis id imperdiet in inceptos integer interdum ipsum justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non nostra nulla nullam nunc odio orci ornare parturient pellentesque penatibus per pharetra phasellus placerat platea porta porttitor posuere potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus viverra volutpat vulputate".split(" ");
|
||||
|
||||
function randomSentence(minWords, maxWords) {
|
||||
const len = minWords + Math.floor(Math.random() * (maxWords - minWords));
|
||||
const words = [];
|
||||
for (let i = 0; i < len; i++) words.push(WORDS[Math.floor(Math.random() * WORDS.length)]);
|
||||
words[0] = words[0][0].toUpperCase() + words[0].slice(1);
|
||||
return words.join(" ") + ".";
|
||||
}
|
||||
|
||||
function randomParagraph() {
|
||||
const count = 4 + Math.floor(Math.random() * 5);
|
||||
const sentences = [];
|
||||
for (let i = 0; i < count; i++) sentences.push(randomSentence(6, 16));
|
||||
return sentences.join(" ");
|
||||
}
|
||||
|
||||
function generate() {
|
||||
const count = parseInt(document.getElementById("li-count").value) || 1;
|
||||
const type = document.getElementById("li-type").value;
|
||||
const startLorem = document.getElementById("li-start").checked;
|
||||
let result = "";
|
||||
|
||||
if (type === "paragraphs") {
|
||||
const paras = [];
|
||||
for (let i = 0; i < count; i++) paras.push(randomParagraph());
|
||||
if (startLorem) paras[0] = LOREM + " " + paras[0];
|
||||
result = paras.join("\n\n");
|
||||
} else if (type === "sentences") {
|
||||
const sents = [];
|
||||
for (let i = 0; i < count; i++) sents.push(randomSentence(6, 16));
|
||||
if (startLorem) sents[0] = LOREM.split(". ")[0] + ".";
|
||||
result = sents.join(" ");
|
||||
} else {
|
||||
const words = [];
|
||||
if (startLorem) {
|
||||
const loremWords = LOREM.replace(/[.,]/g, "").toLowerCase().split(" ");
|
||||
for (let i = 0; i < Math.min(count, loremWords.length); i++) words.push(loremWords[i]);
|
||||
}
|
||||
while (words.length < count) words.push(WORDS[Math.floor(Math.random() * WORDS.length)]);
|
||||
result = words.slice(0, count).join(" ");
|
||||
}
|
||||
document.getElementById("li-output").textContent = result;
|
||||
}
|
||||
|
||||
generate();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,65 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Number Base Converter - EveryTools{% endblock %}
|
||||
{% block top_title %}Number Base Converter{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool" style="max-width:500px">
|
||||
<div class="tool-header">
|
||||
<h1>Number Base Converter</h1>
|
||||
<p>Convert between decimal, binary, octal, and hexadecimal</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Decimal (base 10)</label>
|
||||
<input type="text" id="nb-dec" placeholder="e.g. 255" oninput="fromBase('dec')">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Binary (base 2)</label>
|
||||
<input type="text" id="nb-bin" placeholder="e.g. 11111111" oninput="fromBase('bin')">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Octal (base 8)</label>
|
||||
<input type="text" id="nb-oct" placeholder="e.g. 377" oninput="fromBase('oct')">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Hexadecimal (base 16)</label>
|
||||
<input type="text" id="nb-hex" placeholder="e.g. FF" oninput="fromBase('hex')">
|
||||
</div>
|
||||
<div id="nb-error" style="color:var(--danger);font-size:.85rem;margin-top:.5rem"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function fromBase(source) {
|
||||
const val = document.getElementById(`nb-${source}`).value.trim();
|
||||
const err = document.getElementById("nb-error");
|
||||
err.textContent = "";
|
||||
if (!val) {
|
||||
["dec","bin","oct","hex"].forEach(b => { if (b !== source) document.getElementById(`nb-${b}`).value = ""; });
|
||||
return;
|
||||
}
|
||||
|
||||
let num;
|
||||
try {
|
||||
switch (source) {
|
||||
case "dec": num = BigInt(val); break;
|
||||
case "bin": num = BigInt("0b" + val); break;
|
||||
case "oct": num = BigInt("0o" + val); break;
|
||||
case "hex": num = BigInt("0x" + val); break;
|
||||
}
|
||||
} catch (e) {
|
||||
err.textContent = "Invalid input for this base.";
|
||||
return;
|
||||
}
|
||||
|
||||
const isNeg = num < 0n;
|
||||
const abs = isNeg ? -num : num;
|
||||
const prefix = isNeg ? "-" : "";
|
||||
|
||||
if (source !== "dec") document.getElementById("nb-dec").value = num.toString(10);
|
||||
if (source !== "bin") document.getElementById("nb-bin").value = prefix + abs.toString(2);
|
||||
if (source !== "oct") document.getElementById("nb-oct").value = prefix + abs.toString(8);
|
||||
if (source !== "hex") document.getElementById("nb-hex").value = prefix + abs.toString(16).toUpperCase();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Password Generator - EveryTools{% endblock %}
|
||||
{% block top_title %}Password Generator{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool" style="max-width:500px">
|
||||
<div class="tool-header">
|
||||
<h1>Password Generator</h1>
|
||||
<p>Generate strong, random passwords</p>
|
||||
</div>
|
||||
|
||||
<div class="calc-display" style="position:relative;cursor:pointer" onclick="copyPw()">
|
||||
<div id="pw-output" style="font-family:Consolas,Monaco,monospace;font-size:1.4rem;word-break:break-all;min-height:1.5em;letter-spacing:.05em"></div>
|
||||
<div style="font-size:.75rem;color:var(--text-light);margin-top:.3rem"><i class="bi bi-clipboard"></i> Click to copy</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:.5rem;margin-bottom:1.2rem">
|
||||
<button class="btn btn-primary" onclick="generate()" style="flex:1"><i class="bi bi-arrow-repeat"></i> Generate</button>
|
||||
<button class="btn btn-small" onclick="copyPw()"><i class="bi bi-clipboard"></i> Copy</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Length: <span id="pw-len-val">16</span></label>
|
||||
<div class="range-group">
|
||||
<input type="range" id="pw-length" value="16" min="4" max="128" oninput="document.getElementById('pw-len-val').textContent=this.value;generate()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem">
|
||||
<label class="checkbox-label"><input type="checkbox" id="pw-upper" checked onchange="generate()"><span>Uppercase (A-Z)</span></label>
|
||||
<label class="checkbox-label"><input type="checkbox" id="pw-lower" checked onchange="generate()"><span>Lowercase (a-z)</span></label>
|
||||
<label class="checkbox-label"><input type="checkbox" id="pw-digits" checked onchange="generate()"><span>Digits (0-9)</span></label>
|
||||
<label class="checkbox-label"><input type="checkbox" id="pw-symbols" checked onchange="generate()"><span>Symbols (!@#$...)</span></label>
|
||||
</div>
|
||||
|
||||
<div id="pw-strength" style="margin-top:1rem"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<style>
|
||||
.strength-bar { height: 6px; border-radius: 3px; background: var(--border); margin-top: .5rem; overflow: hidden; }
|
||||
.strength-fill { height: 100%; border-radius: 3px; transition: width .3s, background .3s; }
|
||||
</style>
|
||||
<script>
|
||||
function generate() {
|
||||
const len = parseInt(document.getElementById("pw-length").value);
|
||||
let chars = "";
|
||||
if (document.getElementById("pw-upper").checked) chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
if (document.getElementById("pw-lower").checked) chars += "abcdefghijklmnopqrstuvwxyz";
|
||||
if (document.getElementById("pw-digits").checked) chars += "0123456789";
|
||||
if (document.getElementById("pw-symbols").checked) chars += "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
|
||||
if (!chars) { document.getElementById("pw-output").textContent = "Select at least one option"; return; }
|
||||
|
||||
const arr = new Uint32Array(len);
|
||||
crypto.getRandomValues(arr);
|
||||
let pw = "";
|
||||
for (let i = 0; i < len; i++) pw += chars[arr[i] % chars.length];
|
||||
|
||||
document.getElementById("pw-output").textContent = pw;
|
||||
showStrength(pw, chars.length);
|
||||
}
|
||||
|
||||
function showStrength(pw, poolSize) {
|
||||
const entropy = pw.length * Math.log2(poolSize);
|
||||
let label, color, pct;
|
||||
if (entropy < 40) { label = "Weak"; color = "var(--danger)"; pct = 25; }
|
||||
else if (entropy < 60) { label = "Fair"; color = "var(--warning)"; pct = 50; }
|
||||
else if (entropy < 80) { label = "Strong"; color = "#2ec4b6"; pct = 75; }
|
||||
else { label = "Very Strong"; color = "var(--primary)"; pct = 100; }
|
||||
|
||||
document.getElementById("pw-strength").innerHTML =
|
||||
`<div style="display:flex;justify-content:space-between;font-size:.85rem"><span>${label}</span><span style="color:var(--text-light)">${Math.round(entropy)} bits of entropy</span></div>` +
|
||||
`<div class="strength-bar"><div class="strength-fill" style="width:${pct}%;background:${color}"></div></div>`;
|
||||
}
|
||||
|
||||
function copyPw() {
|
||||
const pw = document.getElementById("pw-output").textContent;
|
||||
if (pw && !pw.startsWith("Select")) navigator.clipboard.writeText(pw);
|
||||
}
|
||||
|
||||
generate();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,134 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Pomodoro Timer - EveryTools{% endblock %}
|
||||
{% block top_title %}Pomodoro Timer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool" style="max-width:420px;text-align:center">
|
||||
<div class="tool-header">
|
||||
<h1>Pomodoro Timer</h1>
|
||||
<p>Stay focused with timed work and break intervals</p>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:.3rem;margin-bottom:1.5rem;justify-content:center">
|
||||
<button class="btn" id="tab-work" onclick="setTab('work')" style="background:var(--primary);color:#fff">Work</button>
|
||||
<button class="btn" id="tab-short" onclick="setTab('short')" style="background:var(--bg)">Short Break</button>
|
||||
<button class="btn" id="tab-long" onclick="setTab('long')" style="background:var(--bg)">Long Break</button>
|
||||
</div>
|
||||
|
||||
<div class="calc-display" style="padding:2rem">
|
||||
<div class="calc-result" id="pom-display" style="font-size:4rem;font-weight:300;letter-spacing:.05em">25:00</div>
|
||||
<div id="pom-label" style="font-size:.9rem;color:var(--text-light);margin-top:.3rem">Work Session</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:.6rem;justify-content:center;margin-bottom:1.5rem">
|
||||
<button class="btn btn-primary" id="pom-start" onclick="toggleTimer()" style="min-width:100px">
|
||||
<i class="bi bi-play-fill"></i> Start
|
||||
</button>
|
||||
<button class="btn btn-small" onclick="resetTimer()"><i class="bi bi-arrow-counterclockwise"></i> Reset</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:1rem;justify-content:center;flex-wrap:wrap">
|
||||
<div class="form-group" style="margin-bottom:0;width:100px">
|
||||
<label>Work (min)</label>
|
||||
<input type="number" id="pom-work-min" value="25" min="1" max="120" onchange="updateDurations()">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0;width:100px">
|
||||
<label>Short (min)</label>
|
||||
<input type="number" id="pom-short-min" value="5" min="1" max="30" onchange="updateDurations()">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0;width:100px">
|
||||
<label>Long (min)</label>
|
||||
<input type="number" id="pom-long-min" value="15" min="1" max="60" onchange="updateDurations()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1.5rem">
|
||||
<span style="font-size:.85rem;color:var(--text-light)">Sessions completed: </span>
|
||||
<span id="pom-sessions" style="font-weight:600;color:var(--primary)">0</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let durations = { work: 25*60, short: 5*60, long: 15*60 };
|
||||
let currentTab = "work";
|
||||
let timeLeft = durations.work;
|
||||
let running = false;
|
||||
let interval = null;
|
||||
let sessions = 0;
|
||||
|
||||
function setTab(tab) {
|
||||
currentTab = tab;
|
||||
stopTimer();
|
||||
timeLeft = durations[tab];
|
||||
updateDisplay();
|
||||
["work","short","long"].forEach(t => {
|
||||
document.getElementById("tab-" + t).style.background = t === tab ? "var(--primary)" : "var(--bg)";
|
||||
document.getElementById("tab-" + t).style.color = t === tab ? "#fff" : "var(--text)";
|
||||
});
|
||||
const labels = { work: "Work Session", short: "Short Break", long: "Long Break" };
|
||||
document.getElementById("pom-label").textContent = labels[tab];
|
||||
}
|
||||
|
||||
function updateDisplay() {
|
||||
const m = Math.floor(timeLeft / 60);
|
||||
const s = timeLeft % 60;
|
||||
document.getElementById("pom-display").textContent =
|
||||
String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
|
||||
}
|
||||
|
||||
function toggleTimer() {
|
||||
if (running) stopTimer();
|
||||
else startTimer();
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
running = true;
|
||||
document.getElementById("pom-start").innerHTML = '<i class="bi bi-pause-fill"></i> Pause';
|
||||
interval = setInterval(() => {
|
||||
timeLeft--;
|
||||
if (timeLeft <= 0) {
|
||||
stopTimer();
|
||||
if (currentTab === "work") {
|
||||
sessions++;
|
||||
document.getElementById("pom-sessions").textContent = sessions;
|
||||
try { new Audio("data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1idH2Mem1naXaAg3x0cHZ/g4F8eHl6fn98enl4eXx+fn18fHx8fn5+fX19fX5+fn19fX1+fn59fX19fn5+fX19fX5+fn19fX1+fn59fX19fn5+").play(); } catch(e){}
|
||||
// Auto-switch to break
|
||||
if (sessions % 4 === 0) setTab("long");
|
||||
else setTab("short");
|
||||
} else {
|
||||
setTab("work");
|
||||
}
|
||||
timeLeft = durations[currentTab];
|
||||
updateDisplay();
|
||||
}
|
||||
updateDisplay();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
running = false;
|
||||
clearInterval(interval);
|
||||
document.getElementById("pom-start").innerHTML = '<i class="bi bi-play-fill"></i> Start';
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
stopTimer();
|
||||
timeLeft = durations[currentTab];
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
function updateDurations() {
|
||||
durations.work = (parseInt(document.getElementById("pom-work-min").value) || 25) * 60;
|
||||
durations.short = (parseInt(document.getElementById("pom-short-min").value) || 5) * 60;
|
||||
durations.long = (parseInt(document.getElementById("pom-long-min").value) || 15) * 60;
|
||||
if (!running) {
|
||||
timeLeft = durations[currentTab];
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
updateDisplay();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,115 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Regex Tester - EveryTools{% endblock %}
|
||||
{% block top_title %}Regex Tester{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool">
|
||||
<div class="tool-header">
|
||||
<h1>Regex Tester</h1>
|
||||
<p>Test regular expressions with live match highlighting</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:.5rem;margin-bottom:1rem;align-items:flex-end">
|
||||
<div class="form-group" style="flex:1;margin-bottom:0">
|
||||
<label>Regular Expression</label>
|
||||
<input type="text" id="regex-pattern" placeholder="e.g. \b\w+@\w+\.\w+\b" oninput="testRegex()">
|
||||
</div>
|
||||
<div class="form-group" style="width:100px;margin-bottom:0">
|
||||
<label>Flags</label>
|
||||
<input type="text" id="regex-flags" value="gi" placeholder="gi" oninput="testRegex()" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Test String</label>
|
||||
<textarea id="regex-input" class="text-input" rows="5" placeholder="Enter text to test against..." oninput="testRegex()"></textarea>
|
||||
</div>
|
||||
<div id="regex-error" style="color:var(--danger);font-size:.85rem;margin-bottom:.5rem"></div>
|
||||
<div class="pane" style="margin-bottom:1rem">
|
||||
<div class="pane-header"><span>Highlighted Matches</span><span id="regex-count" style="font-size:.8rem;color:var(--text-light)">0 matches</span></div>
|
||||
<div class="pane-body">
|
||||
<div id="regex-output" style="min-height:80px;white-space:pre-wrap;word-break:break-word;font-family:Consolas,Monaco,monospace;font-size:.9rem;line-height:1.6"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<div class="pane-header"><span>Match Details</span></div>
|
||||
<div class="pane-body">
|
||||
<div id="regex-matches" style="font-size:.85rem;max-height:250px;overflow:auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<style>
|
||||
.regex-hl { background: #fff3cd; border-bottom: 2px solid var(--warning); border-radius: 2px; }
|
||||
.regex-hl:nth-of-type(even) { background: #d1ecf1; border-bottom-color: #17a2b8; }
|
||||
.match-row { padding: .3rem 0; border-bottom: 1px solid var(--border); display: flex; gap: 1rem; }
|
||||
.match-idx { color: var(--text-light); min-width: 2rem; }
|
||||
.match-val { font-family: Consolas,Monaco,monospace; }
|
||||
.match-pos { color: var(--text-light); font-size: .8rem; }
|
||||
</style>
|
||||
<script>
|
||||
function testRegex() {
|
||||
const pattern = document.getElementById("regex-pattern").value;
|
||||
const flags = document.getElementById("regex-flags").value;
|
||||
const input = document.getElementById("regex-input").value;
|
||||
const output = document.getElementById("regex-output");
|
||||
const errEl = document.getElementById("regex-error");
|
||||
const countEl = document.getElementById("regex-count");
|
||||
const matchesEl = document.getElementById("regex-matches");
|
||||
|
||||
errEl.textContent = "";
|
||||
if (!pattern) {
|
||||
output.textContent = input;
|
||||
countEl.textContent = "0 matches";
|
||||
matchesEl.innerHTML = '<span style="color:var(--text-light)">Enter a pattern to see matches.</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
let regex;
|
||||
try {
|
||||
regex = new RegExp(pattern, flags);
|
||||
} catch (e) {
|
||||
errEl.textContent = e.message;
|
||||
output.textContent = input;
|
||||
countEl.textContent = "0 matches";
|
||||
matchesEl.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = [];
|
||||
let match;
|
||||
const allMatches = flags.includes("g") ? input.matchAll(regex) : ((match = regex.exec(input)) ? [match] : []);
|
||||
|
||||
let highlighted = "";
|
||||
let lastIdx = 0;
|
||||
let count = 0;
|
||||
|
||||
for (const m of allMatches) {
|
||||
const start = m.index;
|
||||
const end = start + m[0].length;
|
||||
highlighted += escapeHtml(input.slice(lastIdx, start));
|
||||
highlighted += `<span class="regex-hl">${escapeHtml(m[0])}</span>`;
|
||||
lastIdx = end;
|
||||
matches.push({ index: count, value: m[0], start, end, groups: m.slice(1) });
|
||||
count++;
|
||||
if (count > 1000) break;
|
||||
}
|
||||
highlighted += escapeHtml(input.slice(lastIdx));
|
||||
|
||||
output.innerHTML = highlighted || escapeHtml(input);
|
||||
countEl.textContent = count + " match" + (count !== 1 ? "es" : "");
|
||||
|
||||
if (matches.length === 0) {
|
||||
matchesEl.innerHTML = '<span style="color:var(--text-light)">No matches found.</span>';
|
||||
} else {
|
||||
matchesEl.innerHTML = matches.map(m => {
|
||||
let groups = "";
|
||||
if (m.groups.length) groups = " groups: " + m.groups.map((g,i) => `<span style="color:var(--primary)">[${i+1}]</span> ${escapeHtml(g||"")}`).join(", ");
|
||||
return `<div class="match-row"><span class="match-idx">#${m.index+1}</span><span class="match-val">${escapeHtml(m.value)}</span><span class="match-pos">[${m.start}:${m.end}]</span>${groups}</div>`;
|
||||
}).join("");
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) { return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Slug Generator - EveryTools{% endblock %}
|
||||
{% block top_title %}Slug Generator{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool">
|
||||
<div class="tool-header">
|
||||
<h1>Slug Generator</h1>
|
||||
<p>Create URL-friendly slugs from any text</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Input Text</label>
|
||||
<textarea id="slug-input" class="text-input" rows="3" placeholder="My Blog Post Title! (2024)" oninput="generateSlug()"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Separator</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-label"><input type="radio" name="sep" value="-" checked onchange="generateSlug()"><span>Hyphen (-)</span></label>
|
||||
<label class="radio-label"><input type="radio" name="sep" value="_" onchange="generateSlug()"><span>Underscore (_)</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane" style="margin-top:1rem">
|
||||
<div class="pane-header">
|
||||
<span>Slug</span>
|
||||
<button class="btn btn-small" onclick="navigator.clipboard.writeText(document.getElementById('slug-output').textContent)"><i class="bi bi-clipboard"></i> Copy</button>
|
||||
</div>
|
||||
<div class="pane-body">
|
||||
<pre id="slug-output" style="min-height:2em;font-size:1rem;color:var(--primary)"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function generateSlug() {
|
||||
const input = document.getElementById("slug-input").value;
|
||||
const sep = document.querySelector('input[name="sep"]:checked').value;
|
||||
const slug = input
|
||||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, "")
|
||||
.trim()
|
||||
.replace(/[\s-]+/g, sep);
|
||||
document.getElementById("slug-output").textContent = slug;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Text Diff - EveryTools{% endblock %}
|
||||
{% block top_title %}Text Diff{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool">
|
||||
<div class="tool-header">
|
||||
<h1>Text Diff</h1>
|
||||
<p>Compare two texts and see the differences</p>
|
||||
</div>
|
||||
<div class="split-pane">
|
||||
<div class="pane">
|
||||
<div class="pane-header"><span>Original</span></div>
|
||||
<div class="pane-body">
|
||||
<textarea id="diff-left" placeholder="Paste original text..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<div class="pane-header"><span>Modified</span></div>
|
||||
<div class="pane-body">
|
||||
<textarea id="diff-right" placeholder="Paste modified text..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" style="margin-top:1rem" onclick="computeDiff()">Compare</button>
|
||||
<div id="diff-output" style="margin-top:1rem"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<style>
|
||||
.diff-view { font-family: "Consolas","Monaco",monospace; font-size: .85rem; border: 1px solid var(--border); border-radius: var(--radius); overflow: auto; max-height: 500px; }
|
||||
.diff-line { padding: .15rem .6rem; white-space: pre-wrap; word-break: break-word; display: flex; }
|
||||
.diff-num { color: var(--text-light); min-width: 3rem; text-align: right; padding-right: .6rem; user-select: none; flex-shrink: 0; }
|
||||
.diff-add { background: #d4edda; }
|
||||
.diff-del { background: #f8d7da; }
|
||||
.diff-ctx { background: var(--surface); }
|
||||
.diff-hdr { background: var(--bg); color: var(--text-light); font-weight: 600; padding: .3rem .6rem; }
|
||||
.diff-stats { font-size: .85rem; margin-bottom: .5rem; }
|
||||
.diff-stats .add { color: #28a745; font-weight: 600; }
|
||||
.diff-stats .del { color: var(--danger); font-weight: 600; }
|
||||
</style>
|
||||
<script>
|
||||
function computeDiff() {
|
||||
const a = document.getElementById("diff-left").value.split("\n");
|
||||
const b = document.getElementById("diff-right").value.split("\n");
|
||||
|
||||
// Simple LCS-based diff
|
||||
const lcs = buildLCS(a, b);
|
||||
const lines = [];
|
||||
let i = 0, j = 0, li = 0;
|
||||
let adds = 0, dels = 0;
|
||||
|
||||
while (li < lcs.length || i < a.length || j < b.length) {
|
||||
if (li < lcs.length) {
|
||||
// Output removals before next common line
|
||||
while (i < lcs[li][0]) { lines.push({type:"del", num: i+1, text: a[i]}); dels++; i++; }
|
||||
while (j < lcs[li][1]) { lines.push({type:"add", num: j+1, text: b[j]}); adds++; j++; }
|
||||
lines.push({type:"ctx", num: i+1, text: a[i]});
|
||||
i++; j++; li++;
|
||||
} else {
|
||||
while (i < a.length) { lines.push({type:"del", num: i+1, text: a[i]}); dels++; i++; }
|
||||
while (j < b.length) { lines.push({type:"add", num: j+1, text: b[j]}); adds++; j++; }
|
||||
}
|
||||
}
|
||||
|
||||
const out = document.getElementById("diff-output");
|
||||
let html = `<div class="diff-stats"><span class="add">+${adds} additions</span> <span class="del">-${dels} deletions</span></div>`;
|
||||
html += '<div class="diff-view">';
|
||||
for (const l of lines) {
|
||||
const prefix = l.type === "add" ? "+" : l.type === "del" ? "-" : " ";
|
||||
const cls = l.type === "add" ? "diff-add" : l.type === "del" ? "diff-del" : "diff-ctx";
|
||||
const escaped = l.text.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||
html += `<div class="diff-line ${cls}"><span class="diff-num">${l.num}</span>${prefix} ${escaped}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
if (adds === 0 && dels === 0) html = '<div style="color:var(--success);font-weight:500"><i class="bi bi-check-circle"></i> Texts are identical.</div>';
|
||||
out.innerHTML = html;
|
||||
}
|
||||
|
||||
function buildLCS(a, b) {
|
||||
const m = a.length, n = b.length;
|
||||
const dp = Array.from({length: m+1}, () => new Array(n+1).fill(0));
|
||||
for (let i = 1; i <= m; i++)
|
||||
for (let j = 1; j <= n; j++)
|
||||
dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1]+1 : Math.max(dp[i-1][j], dp[i][j-1]);
|
||||
|
||||
const result = [];
|
||||
let i = m, j = n;
|
||||
while (i > 0 && j > 0) {
|
||||
if (a[i-1] === b[j-1]) { result.unshift([i-1, j-1]); i--; j--; }
|
||||
else if (dp[i-1][j] > dp[i][j-1]) i--;
|
||||
else j--;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,97 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Timestamp Converter - EveryTools{% endblock %}
|
||||
{% block top_title %}Timestamp Converter{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="client-tool">
|
||||
<div class="tool-header">
|
||||
<h1>Timestamp Converter</h1>
|
||||
<p>Convert between Unix timestamps and human-readable dates</p>
|
||||
</div>
|
||||
|
||||
<div class="calc-section">
|
||||
<h3>Current Time</h3>
|
||||
<div class="calc-result-inline" id="ts-now"></div>
|
||||
<div style="font-size:.85rem;color:var(--text-light)" id="ts-now-date"></div>
|
||||
</div>
|
||||
|
||||
<div class="calc-section">
|
||||
<h3>Unix Timestamp → Date</h3>
|
||||
<div class="inline-form">
|
||||
<input type="number" id="ts-input" placeholder="e.g. 1700000000" class="wide" oninput="tsToDate()">
|
||||
<select id="ts-unit" onchange="tsToDate()" style="width:auto">
|
||||
<option value="s">Seconds</option>
|
||||
<option value="ms">Milliseconds</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="ts-to-date-result" class="calc-result-inline" style="font-size:1rem;margin-top:.5rem">—</div>
|
||||
</div>
|
||||
|
||||
<div class="calc-section">
|
||||
<h3>Date → Unix Timestamp</h3>
|
||||
<div class="inline-form">
|
||||
<input type="datetime-local" id="ts-date-input" onchange="dateToTs()" step="1">
|
||||
</div>
|
||||
<div style="display:flex;gap:1rem;margin-top:.5rem">
|
||||
<div>
|
||||
<div style="font-size:.8rem;color:var(--text-light)">Seconds</div>
|
||||
<div class="calc-result-inline" id="ts-from-date-s" style="font-size:1rem">—</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:.8rem;color:var(--text-light)">Milliseconds</div>
|
||||
<div class="calc-result-inline" id="ts-from-date-ms" style="font-size:1rem">—</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-small" style="margin-top:.5rem" onclick="copyTs()"><i class="bi bi-clipboard"></i> Copy seconds</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function updateNow() {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
document.getElementById("ts-now").textContent = now;
|
||||
document.getElementById("ts-now-date").textContent = new Date().toLocaleString();
|
||||
}
|
||||
updateNow();
|
||||
setInterval(updateNow, 1000);
|
||||
|
||||
function tsToDate() {
|
||||
const val = document.getElementById("ts-input").value;
|
||||
const unit = document.getElementById("ts-unit").value;
|
||||
if (!val) { document.getElementById("ts-to-date-result").textContent = "\u2014"; return; }
|
||||
|
||||
let ms = parseInt(val);
|
||||
if (unit === "s") ms *= 1000;
|
||||
|
||||
const d = new Date(ms);
|
||||
if (isNaN(d)) { document.getElementById("ts-to-date-result").textContent = "Invalid timestamp"; return; }
|
||||
|
||||
document.getElementById("ts-to-date-result").innerHTML =
|
||||
`<div>${d.toLocaleString()} <span style="color:var(--text-light);font-size:.85rem">(Local)</span></div>` +
|
||||
`<div>${d.toUTCString()} <span style="color:var(--text-light);font-size:.85rem">(UTC)</span></div>` +
|
||||
`<div>${d.toISOString()} <span style="color:var(--text-light);font-size:.85rem">(ISO 8601)</span></div>`;
|
||||
}
|
||||
|
||||
function dateToTs() {
|
||||
const val = document.getElementById("ts-date-input").value;
|
||||
if (!val) return;
|
||||
const d = new Date(val);
|
||||
const s = Math.floor(d.getTime() / 1000);
|
||||
document.getElementById("ts-from-date-s").textContent = s;
|
||||
document.getElementById("ts-from-date-ms").textContent = d.getTime();
|
||||
}
|
||||
|
||||
function copyTs() {
|
||||
const s = document.getElementById("ts-from-date-s").textContent;
|
||||
if (s !== "\u2014") navigator.clipboard.writeText(s);
|
||||
}
|
||||
|
||||
// Pre-fill date input with now
|
||||
const now = new Date();
|
||||
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 19);
|
||||
document.getElementById("ts-date-input").value = local;
|
||||
dateToTs();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user