added more tools and bug fixes

This commit is contained in:
listyantidewi1
2026-04-20 14:43:43 +07:00
parent de66fde018
commit 86cb67d5d5
28 changed files with 2154 additions and 20 deletions
+79
View File
@@ -0,0 +1,79 @@
# Changelog
All notable changes to **Your Everyday Tools** are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project loosely follows [Semantic Versioning](https://semver.org/).
## [0.4.1] — 2026-04-20
### Fixed
- **PDF Compress** no longer breaks page layout. Compressed images are now replaced in-place via `page.replace_image(xref, ...)` instead of being re-inserted at full page dimensions, so the original placement, size, and positioning matrix are preserved. Images shared across multiple pages are also deduplicated so they're only recompressed once.
- **PDF Resize** now actually scales page content instead of only changing the media box (which previously just cropped the visible area, leaving images and text clipped or misaligned). Scale mode renders each page onto a new page at the new size; paper-size mode fits content into the target with aspect ratio preserved and orientation matched.
- **Upload UI** — the invisible file input no longer covers the uploaded file list. The remove buttons on each listed file are now clickable. Structural fix: `.file-list` is now a sibling of `.upload-zone` rather than a child.
### Added
- Per-quality image-dimension cap on PDF Compress (1200/1800/2400 px max edge for low/medium/high) so photo-heavy PDFs actually shrink.
## [0.4.0] — 2026-04-20
### Added — Developer Utilities (new category, 10 tools)
- **UUID Generator** (client-side) — v4 UUIDs, bulk generation, formatting options.
- **JWT Decoder** (client-side) — decodes JWT header/payload/signature; does not verify signatures.
- **User-Agent Parser** (client-side) — extracts browser, OS, device, and engine.
- **SQL Formatter** — pretty-prints SQL via `sqlparse` with configurable keyword case and indent.
- **XML Formatter** (client-side) — format, validate, and minify XML via DOMParser.
- **HTML Formatter** (client-side) — beautify/minify with correct handling of void tags, inline tags, and raw-content blocks.
- **CSS Formatter** (client-side) — indent-aware beautify/minify.
- **JS Formatter** (client-side) — basic beautify/minify with string and comment protection.
- **Cron Parser** — validates expressions and previews upcoming runs via `croniter`.
- **JSONPath Tester** — evaluates JSONPath expressions via `jsonpath-ng`.
### Added — Archive Tools (new category, 3 tools)
- **Create ZIP** — bundle multiple files with Deflate or Store compression.
- **Extract ZIP** — extract archive contents (500 MB cap; encrypted ZIPs rejected).
- **ZIP Info** — list entries with sizes, dates, and overall compression ratio.
### Added — Audio & Video (new category, 6 tools — requires FFmpeg on PATH)
- **Convert Audio** — MP3/WAV/OGG/FLAC/AAC/M4A/Opus with adjustable bitrate.
- **Convert Video** — MP4/WebM/MKV/MOV/AVI with sensible codec defaults.
- **Extract Audio** — pull audio track from a video file.
- **Trim Media** — cut by start/end time; stream-copy first, re-encodes on failure.
- **Compress Video** — H.264 re-encode at configurable CRF and preset.
- **Video to GIF** — with FPS, width, start, and duration options.
### Added — Security
- **File Hash** — streaming MD5/SHA-1/SHA-256/SHA-512 of uploaded files.
### Added — Dependencies
- `sqlparse`, `croniter`, `jsonpath-ng` added to `requirements.txt`.
- FFmpeg documented as an optional external binary; each media tool page shows install instructions and a detected/not-detected banner.
## [0.3.0] — 2026-04-19
### Added — Spreadsheet (new category, 6 tools)
- **Excel to CSV / JSON** — export sheets from `.xlsx` / `.xls` to CSV or JSON (array-of-objects or array-of-arrays), single sheet or all sheets as ZIP.
- **CSV / JSON to Excel** — build `.xlsx` from CSV/JSON files, one sheet per file, optional bold/shaded header row.
- **Excel to PDF** — one section per sheet, configurable page size, orientation, and font size. 5000-row cap per sheet.
- **Merge Workbooks** — combine multiple Excel files, optionally prefixing sheet names with source filename.
- **Split Sheets** — export each sheet as its own `.xlsx`.
- **Excel Info & Preview** — list sheet names, row/column counts, and preview rows.
### Added — Dependencies
- `openpyxl` (required) and `xlrd` (for legacy `.xls` read) added to `requirements.txt`.
## [0.2.0] — 2026-04-19
### Added
- **OCR PDF** — make scanned PDFs searchable (image + hidden text layer) or extract text. 14 languages supported via optional `pytesseract`.
- **CAD to PDF / Image** — render DXF directly via `ezdxf` + `matplotlib`. DWG supported via optional **ODA File Converter** (auto-detected on PATH). Full install guide added to README and to the CAD tool page.
- **Animated WebP / GIF** — convert between the two formats, preserving per-frame durations.
### Removed
- MIT license badge from README (project has no license).
## [0.1.0] — 2026-04-18
### Added
- Initial release — 48 tools across 7 categories: Document Conversion, PDF Tools, Image Tools, Text & Data, Calculators, QR Code, Security.
- One universal upload template (`upload_tool.html`) powering all server-side tools.
- Client-side-only tools for text utilities, calculators, password and hash generators.
- Graceful degradation for heavy optional dependencies (`rembg`, `pyzbar`, `pdf2docx`).
- Screenshots, README, `.gitignore`.
+43 -4
View File
@@ -1,10 +1,12 @@
# Your Everyday Tools
A lightweight, self-hosted web app that bundles 57 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 77 everyday utilities into a single interface. Built with Python + Flask, zero JavaScript frameworks, and minimal CSS — no bloat, just tools.
![Python](https://img.shields.io/badge/Python-3.10+-blue)
![Flask](https://img.shields.io/badge/Flask-3.x-green)
See [CHANGELOG.md](CHANGELOG.md) for release history and recent fixes.
---
## Screenshots
@@ -107,6 +109,38 @@ A lightweight, self-hosted web app that bundles 57 everyday utilities into a sin
|------|-------------|
| **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 |
| **File Hash** | Compute MD5, SHA-1, SHA-256, and SHA-512 hashes of an uploaded file (streamed, no size cap beyond upload limit) |
### Developer Utilities
| Tool | Description |
|------|-------------|
| **UUID Generator** | Generate v4 UUIDs — single or bulk (up to 1000), with uppercase, brace, and no-dash formatting |
| **JWT Decoder** | Decode JSON Web Tokens client-side to inspect header, payload, and claims (decode only — does not verify signatures) |
| **User-Agent Parser** | Parse browser, OS, device, and engine from any User-Agent string |
| **SQL Formatter** | Pretty-print SQL with configurable keyword casing (UPPER / lower / Capitalize) and indentation — powered by `sqlparse` |
| **XML Formatter** | Format, validate, and minify XML using the browser's native DOMParser |
| **HTML Formatter** | Beautify or minify HTML source (void tags, inline tags, and `<script>` / `<style>` content handled correctly) |
| **CSS Formatter** | Beautify or minify CSS rules with indent-aware output |
| **JS Formatter** | Basic JavaScript beautifier and minifier (for complex code, use Prettier) |
| **Cron Parser** | Validate cron expressions, see next upcoming run times, and get a field-by-field breakdown |
| **JSONPath Tester** | Evaluate JSONPath expressions against JSON data — supports extended syntax via `jsonpath-ng` |
### Archive Tools
| Tool | Description |
|------|-------------|
| **Create ZIP** | Bundle multiple files into a single `.zip`, choose Deflate or Store compression |
| **Extract ZIP** | Extract the contents of a `.zip` and re-download them (encrypted ZIPs not supported; 500 MB total cap) |
| **ZIP Info** | List all entries in a `.zip` with uncompressed/compressed sizes, modified date, and overall compression ratio |
### Audio & Video (requires `ffmpeg` on PATH)
| Tool | Description |
|------|-------------|
| **Convert Audio** | Convert between MP3, WAV, OGG, FLAC, AAC, M4A, and Opus with adjustable bitrate |
| **Convert Video** | Convert between MP4, WebM, MKV, MOV, and AVI (uses sensible codec defaults per target) |
| **Extract Audio** | Pull the audio track out of a video file to MP3 / WAV / OGG / M4A |
| **Trim Media** | Trim audio or video by start/end time (stream-copy first, re-encodes on keyframe mismatch) |
| **Compress Video** | Re-encode video with H.264 at a chosen CRF and preset to shrink file size |
| **Video to GIF** | Convert a clip to an animated GIF with configurable FPS, width, start, and duration |
---
@@ -153,6 +187,8 @@ The core app works out of the box with the main dependencies. Some features requ
| `pdf2docx` | PDF to Word | Pure Python, but conversion quality depends on PDF complexity. |
| `pytesseract` | Image to Text (OCR), OCR PDF | Requires the [Tesseract](https://github.com/tesseract-ocr/tesseract) binary installed on your system. For non-English OCR, download the matching `*.traineddata` language pack into your Tesseract `tessdata` folder. |
| `ezdxf` + `matplotlib` | CAD to PDF/Image | Renders DXF drawings. For DWG support, also install the free [ODA File Converter](https://www.opendesign.com/guestfiles/oda_file_converter) and make sure it's on your `PATH`. |
| `ffmpeg` (external) | All Audio & Video tools | Requires the [FFmpeg](https://ffmpeg.org/download.html) binary on your `PATH`. Each media tool page shows a green banner if FFmpeg is detected, with install instructions if not. |
| `sqlparse`, `croniter`, `jsonpath-ng` | SQL Formatter, Cron Parser, JSONPath Tester | Small pure-Python packages included in `requirements.txt`. Everything else under *Developer Utilities* runs entirely in the browser. |
If you only need the core tools, install the minimal set:
@@ -209,8 +245,11 @@ your-everyday-tools/
│ ├── text_tools.py # Text & data tool page routes
│ ├── calculator_tools.py # Calculator page routes
│ ├── qr_tools.py # QR code endpoints
│ ├── security_tools.py # Security tool page routes
── spreadsheet_tools.py # Excel / CSV / JSON workbook tools
│ ├── security_tools.py # Security tool page routes + file hash
── spreadsheet_tools.py # Excel / CSV / JSON workbook tools
│ ├── dev_tools.py # Developer utilities (UUID/JWT/UA/formatters/cron/jsonpath)
│ ├── archive_tools.py # ZIP create / extract / info
│ └── media_tools.py # FFmpeg-powered audio & video tools
├── templates/
│ ├── base.html # Main layout (sidebar + content area)
│ ├── index.html # Home page with tool cards
@@ -249,7 +288,7 @@ your-everyday-tools/
- **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`, `pytesseract`) 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`) and external binaries (ODA File Converter, FFmpeg) are probed at import time via `importlib` / `shutil.which`. If missing, the affected tool shows a clear install instruction instead of crashing.
---
+47
View File
@@ -116,6 +116,47 @@ TOOL_CATEGORIES = [
"tools": [
{"id": "password-generator", "name": "Password Generator", "desc": "Generate strong random passwords", "icon": "bi-key-fill"},
{"id": "hash-generator", "name": "Hash Generator", "desc": "Generate MD5, SHA hashes", "icon": "bi-fingerprint"},
{"id": "file-hash", "name": "File Hash", "desc": "Compute hashes of uploaded files", "icon": "bi-file-earmark-lock-fill"},
],
},
{
"id": "dev",
"name": "Developer Utilities",
"icon": "bi-code-slash",
"tools": [
{"id": "uuid", "name": "UUID Generator", "desc": "Generate v4 UUIDs (bulk supported)", "icon": "bi-hash"},
{"id": "jwt", "name": "JWT Decoder", "desc": "Decode JWT tokens (client-side)", "icon": "bi-key"},
{"id": "user-agent", "name": "User-Agent Parser", "desc": "Parse browser, OS, and device info", "icon": "bi-window"},
{"id": "sql-format", "name": "SQL Formatter", "desc": "Pretty-print SQL with keyword casing", "icon": "bi-filetype-sql"},
{"id": "xml-format", "name": "XML Formatter", "desc": "Format, validate, and minify XML", "icon": "bi-filetype-xml"},
{"id": "html-format", "name": "HTML Formatter", "desc": "Beautify or minify HTML", "icon": "bi-filetype-html"},
{"id": "css-format", "name": "CSS Formatter", "desc": "Beautify or minify CSS", "icon": "bi-filetype-css"},
{"id": "js-format", "name": "JS Formatter", "desc": "Beautify or minify JavaScript", "icon": "bi-filetype-js"},
{"id": "cron", "name": "Cron Parser", "desc": "Validate cron and preview runs", "icon": "bi-calendar-week-fill"},
{"id": "jsonpath", "name": "JSONPath Tester", "desc": "Query JSON with JSONPath expressions", "icon": "bi-search"},
],
},
{
"id": "archive",
"name": "Archive Tools",
"icon": "bi-file-zip-fill",
"tools": [
{"id": "zip", "name": "Create ZIP", "desc": "Bundle multiple files into a .zip", "icon": "bi-file-zip"},
{"id": "unzip", "name": "Extract ZIP", "desc": "Extract contents of a .zip archive", "icon": "bi-box-arrow-up"},
{"id": "zip-info", "name": "ZIP Info", "desc": "Inspect archive contents and sizes", "icon": "bi-info-circle-fill"},
],
},
{
"id": "media",
"name": "Audio & Video",
"icon": "bi-camera-reels-fill",
"tools": [
{"id": "convert-audio", "name": "Convert Audio", "desc": "Change between audio formats", "icon": "bi-music-note-beamed"},
{"id": "convert-video", "name": "Convert Video", "desc": "Change between video formats", "icon": "bi-camera-video-fill"},
{"id": "extract-audio", "name": "Extract Audio", "desc": "Pull audio track from a video", "icon": "bi-mic-fill"},
{"id": "trim", "name": "Trim Media", "desc": "Cut audio or video by time range", "icon": "bi-scissors"},
{"id": "compress-video", "name": "Compress Video", "desc": "Re-encode to a smaller file", "icon": "bi-file-zip-fill"},
{"id": "video-to-gif", "name": "Video to GIF", "desc": "Convert clips to animated GIFs", "icon": "bi-file-earmark-play-fill"},
],
},
]
@@ -150,6 +191,9 @@ from routes.calculator_tools import bp as calc_bp
from routes.qr_tools import bp as qr_bp
from routes.security_tools import bp as security_bp
from routes.spreadsheet_tools import bp as spreadsheet_bp
from routes.dev_tools import bp as dev_bp
from routes.archive_tools import bp as archive_bp
from routes.media_tools import bp as media_bp
app.register_blueprint(convert_bp, url_prefix="/convert")
app.register_blueprint(pdf_bp, url_prefix="/pdf")
@@ -159,6 +203,9 @@ app.register_blueprint(calc_bp, url_prefix="/calc")
app.register_blueprint(qr_bp, url_prefix="/qr")
app.register_blueprint(security_bp, url_prefix="/security")
app.register_blueprint(spreadsheet_bp, url_prefix="/spreadsheet")
app.register_blueprint(dev_bp, url_prefix="/dev")
app.register_blueprint(archive_bp, url_prefix="/archive")
app.register_blueprint(media_bp, url_prefix="/media")
if __name__ == "__main__":
app.run(debug=True, port=5000)
+3
View File
@@ -9,6 +9,9 @@ img2pdf
python-docx
openpyxl
xlrd
sqlparse
croniter
jsonpath-ng
# Optional (app works without these, shows install message if missing)
rembg
+176
View File
@@ -0,0 +1,176 @@
import io
import zipfile
from datetime import datetime
from flask import Blueprint, render_template, request, send_file, jsonify
from utils.file_utils import make_zip
bp = Blueprint("archive", __name__)
MAX_ENTRIES = 2000
MAX_EXTRACT_BYTES = 500 * 1024 * 1024 # 500 MB total extracted size (zip bomb guard)
@bp.route("/zip", methods=["GET", "POST"])
def zip_create():
if request.method == "GET":
return render_template(
"upload_tool.html",
title="Create ZIP",
description="Bundle multiple files into a single .zip archive.",
endpoint="/archive/zip",
accept="*",
multiple=True,
options=[
{
"name": "compression",
"label": "Compression",
"type": "select",
"default": "deflated",
"choices": [
{"value": "deflated", "label": "Deflate (smaller)"},
{"value": "stored", "label": "Store (no compression)"},
],
},
{
"name": "archive_name",
"label": "Output name (without .zip)",
"type": "text",
"default": "archive",
},
],
button_text="Create ZIP",
)
files = request.files.getlist("files")
if not files:
return jsonify({"error": "No files uploaded."}), 400
method = zipfile.ZIP_DEFLATED if request.form.get("compression", "deflated") == "deflated" else zipfile.ZIP_STORED
name = (request.form.get("archive_name") or "archive").strip() or "archive"
if name.lower().endswith(".zip"):
name = name[:-4]
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", method) as zf:
for f in files:
data = f.read()
zf.writestr(f.filename, data)
buf.seek(0)
return send_file(buf, mimetype="application/zip", as_attachment=True, download_name=f"{name}.zip")
@bp.route("/unzip", methods=["GET", "POST"])
def zip_extract():
if request.method == "GET":
return render_template(
"upload_tool.html",
title="Extract ZIP",
description="Extract a .zip archive and repackage the contents as a flat download.",
notes="<strong>Note:</strong> extracted contents are returned as a new ZIP (preserving directory layout). "
"Encrypted archives are not supported. Max total extracted size: 500 MB.",
endpoint="/archive/unzip",
accept=".zip",
multiple=False,
button_text="Extract",
)
if "files" not in request.files:
return jsonify({"error": "No file uploaded."}), 400
f = request.files["files"]
if not f.filename.lower().endswith(".zip"):
return jsonify({"error": "Please upload a .zip file."}), 400
try:
data = f.read()
with zipfile.ZipFile(io.BytesIO(data)) as zf:
total = 0
out = []
for info in zf.infolist():
if info.is_dir():
continue
if info.file_size > MAX_EXTRACT_BYTES:
return jsonify({"error": "File in archive exceeds size limit."}), 400
total += info.file_size
if total > MAX_EXTRACT_BYTES:
return jsonify({"error": "Total extracted size exceeds 500 MB limit."}), 400
out.append((info.filename, zf.read(info)))
except zipfile.BadZipFile:
return jsonify({"error": "Not a valid ZIP archive."}), 400
except RuntimeError as e:
if "password" in str(e).lower() or "encrypted" in str(e).lower():
return jsonify({"error": "Password-protected ZIPs are not supported."}), 400
return jsonify({"error": f"Extraction failed: {e}"}), 400
if not out:
return jsonify({"error": "Archive is empty."}), 400
buf = make_zip(out)
base = f.filename.rsplit(".", 1)[0]
return send_file(buf, mimetype="application/zip", as_attachment=True, download_name=f"{base}_extracted.zip")
@bp.route("/zip-info", methods=["GET", "POST"])
def zip_info():
if request.method == "GET":
return render_template(
"upload_tool.html",
title="ZIP Info",
description="Inspect a ZIP archive: list files, sizes, and compression ratios.",
endpoint="/archive/zip-info",
accept=".zip",
multiple=False,
button_text="Inspect",
)
if "files" not in request.files:
return jsonify({"error": "No file uploaded."}), 400
f = request.files["files"]
if not f.filename.lower().endswith(".zip"):
return jsonify({"error": "Please upload a .zip file."}), 400
try:
data = f.read()
with zipfile.ZipFile(io.BytesIO(data)) as zf:
entries = []
total_uncompressed = 0
total_compressed = 0
for info in zf.infolist()[:MAX_ENTRIES]:
dt = "-"
try:
dt = datetime(*info.date_time).strftime("%Y-%m-%d %H:%M")
except Exception:
pass
entries.append(
f"{_format_size(info.file_size):>10} "
f"{_format_size(info.compress_size):>10} "
f"{dt} "
f"{info.filename}"
)
total_uncompressed += info.file_size
total_compressed += info.compress_size
ratio = (1 - total_compressed / total_uncompressed) * 100 if total_uncompressed else 0
header = (
f"{'Uncompr.':>10} {'Compr.':>10} {'Modified':<16} Name\n"
f"{'-' * 10} {'-' * 10} {'-' * 16} {'-' * 30}"
)
footer = (
f"\n{'-' * 10} {'-' * 10}\n"
f"{_format_size(total_uncompressed):>10} {_format_size(total_compressed):>10} "
f"({len(zf.infolist())} entries, {ratio:.1f}% saved)"
)
return jsonify({"text": header + "\n" + "\n".join(entries) + footer})
except zipfile.BadZipFile:
return jsonify({"error": "Not a valid ZIP archive."}), 400
def _format_size(n: int) -> str:
for unit in ("B", "KB", "MB", "GB"):
if n < 1024:
return f"{n:.1f} {unit}" if unit != "B" else f"{n} B"
n /= 1024
return f"{n:.1f} TB"
+212
View File
@@ -0,0 +1,212 @@
from datetime import datetime, timezone
from flask import Blueprint, render_template, request, jsonify
try:
import sqlparse
HAS_SQLPARSE = True
except ImportError:
HAS_SQLPARSE = False
try:
from croniter import croniter
HAS_CRONITER = True
except ImportError:
HAS_CRONITER = False
try:
from jsonpath_ng.ext import parse as jsonpath_parse
HAS_JSONPATH = True
except ImportError:
try:
from jsonpath_ng import parse as jsonpath_parse
HAS_JSONPATH = True
except ImportError:
HAS_JSONPATH = False
import json as _json
bp = Blueprint("dev", __name__)
# ── Client-only pages ──────────────────────────────────
@bp.route("/uuid")
def uuid_generator():
return render_template("tools/uuid_generator.html")
@bp.route("/jwt")
def jwt_decoder():
return render_template("tools/jwt_decoder.html")
@bp.route("/user-agent")
def user_agent_parser():
return render_template("tools/user_agent_parser.html")
@bp.route("/xml-format")
def xml_formatter():
return render_template("tools/xml_formatter.html")
@bp.route("/html-format")
def html_formatter():
return render_template("tools/html_formatter.html")
@bp.route("/css-format")
def css_formatter():
return render_template("tools/css_formatter.html")
@bp.route("/js-format")
def js_formatter():
return render_template("tools/js_formatter.html")
# ── Server-side helpers ────────────────────────────────
@bp.route("/sql-format", methods=["GET", "POST"])
def sql_format():
if request.method == "GET":
return render_template("tools/sql_formatter.html")
if not HAS_SQLPARSE:
return jsonify({"error": "sqlparse is not installed on the server."}), 500
sql = request.form.get("sql", "").strip()
if not sql:
return jsonify({"error": "No SQL provided."}), 400
keyword_case = request.form.get("keyword_case", "upper")
indent = request.form.get("indent", "2")
if keyword_case not in ("upper", "lower", "capitalize"):
keyword_case = "upper"
if indent == "tab":
indent_width = 1
use_tab = True
else:
try:
indent_width = int(indent)
if indent_width not in (2, 4):
indent_width = 2
except ValueError:
indent_width = 2
use_tab = False
try:
formatted = sqlparse.format(
sql,
keyword_case=keyword_case,
identifier_case=None,
reindent=True,
indent_width=indent_width,
strip_comments=False,
)
if use_tab:
formatted = formatted.replace(" ", "\t")
return jsonify({"text": formatted})
except Exception as e:
return jsonify({"error": f"Formatting failed: {e}"}), 400
@bp.route("/cron", methods=["GET", "POST"])
def cron_parser():
if request.method == "GET":
return render_template("tools/cron_parser.html")
if not HAS_CRONITER:
return jsonify({"error": "croniter is not installed on the server."}), 500
expr = request.form.get("expr", "").strip()
count_raw = request.form.get("count", "10")
if not expr:
return jsonify({"error": "No expression provided."}), 400
try:
count = max(1, min(50, int(count_raw)))
except ValueError:
count = 10
if not croniter.is_valid(expr):
return jsonify({"error": "Invalid cron expression."}), 400
try:
now = datetime.now(timezone.utc)
it = croniter(expr, now)
next_times = []
for _ in range(count):
ts = it.get_next(datetime)
next_times.append(ts.strftime("%Y-%m-%d %H:%M:%S UTC"))
description = _describe_cron(expr)
return jsonify({"description": description, "next": next_times})
except Exception as e:
return jsonify({"error": f"Parse failed: {e}"}), 400
def _describe_cron(expr: str) -> str:
parts = expr.split()
if len(parts) not in (5, 6):
return expr
labels = ["Minute", "Hour", "Day of month", "Month", "Day of week"]
if len(parts) == 6:
labels = ["Second"] + labels
lines = [f"Expression: {expr}"]
for label, val in zip(labels, parts):
meaning = _field_meaning(val)
lines.append(f"{label:<14} {val:<10}{meaning}")
return "\n".join(lines)
def _field_meaning(val: str) -> str:
if val == "*":
return "every value"
if val.startswith("*/"):
return f"every {val[2:]}"
if "-" in val and "/" not in val:
return f"range {val}"
if "," in val:
return f"list: {val}"
if "/" in val:
return f"stepped: {val}"
return f"exactly {val}"
@bp.route("/jsonpath", methods=["GET", "POST"])
def jsonpath_tester():
if request.method == "GET":
return render_template("tools/jsonpath_tester.html")
if not HAS_JSONPATH:
return jsonify({"error": "jsonpath-ng is not installed on the server."}), 500
raw = request.form.get("data", "").strip()
path = request.form.get("path", "").strip()
if not raw:
return jsonify({"error": "No JSON data provided."}), 400
if not path:
return jsonify({"error": "No JSONPath expression provided."}), 400
try:
data = _json.loads(raw)
except _json.JSONDecodeError as e:
return jsonify({"error": f"Invalid JSON: {e}"}), 400
try:
expr = jsonpath_parse(path)
except Exception as e:
return jsonify({"error": f"Invalid JSONPath: {e}"}), 400
try:
matches = [m.value for m in expr.find(data)]
return jsonify({"count": len(matches), "matches": matches})
except Exception as e:
return jsonify({"error": f"Evaluation failed: {e}"}), 400
+474
View File
@@ -0,0 +1,474 @@
import os
import shutil
import subprocess
import tempfile
from flask import Blueprint, render_template, request, send_file, jsonify
bp = Blueprint("media", __name__)
FFMPEG = shutil.which("ffmpeg")
FFPROBE = shutil.which("ffprobe")
AUDIO_FORMATS = ["mp3", "wav", "ogg", "flac", "aac", "m4a", "opus"]
VIDEO_FORMATS = ["mp4", "webm", "mkv", "mov", "avi"]
FFMPEG_INSTALL_NOTE = (
'<p><strong>FFmpeg is required for this tool.</strong></p>'
'<details><summary>How to install FFmpeg</summary>'
'<p><strong>Windows:</strong> Download from '
'<a href="https://www.gyan.dev/ffmpeg/builds/" target="_blank">gyan.dev</a> or '
'<a href="https://github.com/BtbN/FFmpeg-Builds/releases" target="_blank">BtbN builds</a>, '
'extract, and add the <code>bin</code> folder to your PATH.</p>'
'<p><strong>macOS:</strong> <code>brew install ffmpeg</code></p>'
'<p><strong>Linux:</strong> <code>sudo apt install ffmpeg</code> (Debian/Ubuntu) '
'or <code>sudo dnf install ffmpeg</code> (Fedora).</p>'
'<p>Restart the server after installing so the new PATH is picked up.</p>'
'</details>'
)
def _ffmpeg_available_notes():
if FFMPEG:
return (
f'<p><i class="bi bi-check-circle-fill" style="color:#2ec4b6"></i> '
f'<strong>FFmpeg detected:</strong> <code>{FFMPEG}</code></p>'
)
return (
'<p><i class="bi bi-exclamation-triangle-fill" style="color:#ffb703"></i> '
'<strong>FFmpeg was not found on PATH.</strong> This tool will not work until FFmpeg is installed.</p>'
+ FFMPEG_INSTALL_NOTE
)
def _run_ffmpeg(args: list[str], timeout: int = 180):
if not FFMPEG:
return None, "FFmpeg is not installed or not on PATH."
try:
proc = subprocess.run(
[FFMPEG, "-y", "-hide_banner", "-loglevel", "error"] + args,
capture_output=True, timeout=timeout,
)
if proc.returncode != 0:
err = proc.stderr.decode("utf-8", errors="replace") or "unknown error"
return None, f"FFmpeg failed: {err[:500]}"
return proc, None
except subprocess.TimeoutExpired:
return None, "FFmpeg timed out."
def _save_upload(file_storage, tmpdir: str) -> str:
path = os.path.join(tmpdir, "input_" + file_storage.filename.replace("/", "_").replace("\\", "_"))
file_storage.save(path)
return path
# ── Audio convert ──────────────────────────────────────
@bp.route("/convert-audio", methods=["GET", "POST"])
def convert_audio():
if request.method == "GET":
return render_template(
"upload_tool.html",
title="Convert Audio",
description="Convert between common audio formats using FFmpeg.",
notes=_ffmpeg_available_notes(),
endpoint="/media/convert-audio",
accept=".mp3,.wav,.ogg,.flac,.aac,.m4a,.opus,.wma",
multiple=False,
options=[
{
"name": "format",
"label": "Target format",
"type": "select",
"default": "mp3",
"choices": [{"value": f, "label": f.upper()} for f in AUDIO_FORMATS],
},
{
"name": "bitrate",
"label": "Bitrate",
"type": "select",
"default": "192k",
"choices": [
{"value": "96k", "label": "96 kbps"},
{"value": "128k", "label": "128 kbps"},
{"value": "192k", "label": "192 kbps"},
{"value": "256k", "label": "256 kbps"},
{"value": "320k", "label": "320 kbps"},
],
},
],
button_text="Convert",
)
f = request.files.get("files")
if not f:
return jsonify({"error": "No file uploaded."}), 400
fmt = request.form.get("format", "mp3")
if fmt not in AUDIO_FORMATS:
return jsonify({"error": "Unsupported target format."}), 400
bitrate = request.form.get("bitrate", "192k")
with tempfile.TemporaryDirectory() as tmp:
in_path = _save_upload(f, tmp)
out_path = os.path.join(tmp, f"output.{fmt}")
args = ["-i", in_path]
if fmt == "wav" or fmt == "flac":
args += [out_path]
else:
args += ["-b:a", bitrate, out_path]
_, err = _run_ffmpeg(args)
if err:
return jsonify({"error": err}), 400
with open(out_path, "rb") as fp:
data = fp.read()
base = f.filename.rsplit(".", 1)[0]
return send_file(
_bytes_io(data),
mimetype=f"audio/{fmt}",
as_attachment=True,
download_name=f"{base}.{fmt}",
)
# ── Video convert ──────────────────────────────────────
@bp.route("/convert-video", methods=["GET", "POST"])
def convert_video():
if request.method == "GET":
return render_template(
"upload_tool.html",
title="Convert Video",
description="Convert between common video formats using FFmpeg.",
notes=_ffmpeg_available_notes(),
endpoint="/media/convert-video",
accept=".mp4,.webm,.mkv,.mov,.avi,.flv,.wmv",
multiple=False,
options=[
{
"name": "format",
"label": "Target format",
"type": "select",
"default": "mp4",
"choices": [{"value": f, "label": f.upper()} for f in VIDEO_FORMATS],
},
],
button_text="Convert",
)
f = request.files.get("files")
if not f:
return jsonify({"error": "No file uploaded."}), 400
fmt = request.form.get("format", "mp4")
if fmt not in VIDEO_FORMATS:
return jsonify({"error": "Unsupported target format."}), 400
with tempfile.TemporaryDirectory() as tmp:
in_path = _save_upload(f, tmp)
out_path = os.path.join(tmp, f"output.{fmt}")
args = ["-i", in_path]
if fmt == "webm":
args += ["-c:v", "libvpx-vp9", "-c:a", "libopus", out_path]
elif fmt == "mp4":
args += ["-c:v", "libx264", "-c:a", "aac", "-preset", "medium", out_path]
else:
args += [out_path]
_, err = _run_ffmpeg(args, timeout=600)
if err:
return jsonify({"error": err}), 400
with open(out_path, "rb") as fp:
data = fp.read()
base = f.filename.rsplit(".", 1)[0]
return send_file(
_bytes_io(data),
mimetype=f"video/{fmt}",
as_attachment=True,
download_name=f"{base}.{fmt}",
)
# ── Extract audio from video ───────────────────────────
@bp.route("/extract-audio", methods=["GET", "POST"])
def extract_audio():
if request.method == "GET":
return render_template(
"upload_tool.html",
title="Extract Audio",
description="Extract the audio track from a video file.",
notes=_ffmpeg_available_notes(),
endpoint="/media/extract-audio",
accept=".mp4,.webm,.mkv,.mov,.avi,.flv,.wmv",
multiple=False,
options=[
{
"name": "format",
"label": "Audio format",
"type": "select",
"default": "mp3",
"choices": [
{"value": "mp3", "label": "MP3"},
{"value": "wav", "label": "WAV"},
{"value": "ogg", "label": "OGG"},
{"value": "m4a", "label": "M4A (AAC)"},
],
},
],
button_text="Extract",
)
f = request.files.get("files")
if not f:
return jsonify({"error": "No file uploaded."}), 400
fmt = request.form.get("format", "mp3")
if fmt not in ("mp3", "wav", "ogg", "m4a"):
return jsonify({"error": "Unsupported audio format."}), 400
with tempfile.TemporaryDirectory() as tmp:
in_path = _save_upload(f, tmp)
out_path = os.path.join(tmp, f"output.{fmt}")
args = ["-i", in_path, "-vn"]
if fmt == "mp3":
args += ["-b:a", "192k"]
elif fmt == "m4a":
args += ["-c:a", "aac", "-b:a", "192k"]
args += [out_path]
_, err = _run_ffmpeg(args)
if err:
return jsonify({"error": err}), 400
with open(out_path, "rb") as fp:
data = fp.read()
base = f.filename.rsplit(".", 1)[0]
return send_file(
_bytes_io(data),
mimetype=f"audio/{fmt}",
as_attachment=True,
download_name=f"{base}.{fmt}",
)
# ── Trim media ─────────────────────────────────────────
@bp.route("/trim", methods=["GET", "POST"])
def trim():
if request.method == "GET":
return render_template(
"upload_tool.html",
title="Trim Media",
description="Trim an audio or video file by start and end time (HH:MM:SS or seconds).",
notes=_ffmpeg_available_notes(),
endpoint="/media/trim",
accept=".mp4,.webm,.mkv,.mov,.avi,.mp3,.wav,.ogg,.flac,.m4a",
multiple=False,
options=[
{
"name": "start",
"label": "Start (e.g. 0 or 00:00:05)",
"type": "text",
"default": "0",
},
{
"name": "end",
"label": "End (leave blank for end-of-file)",
"type": "text",
"default": "",
},
],
button_text="Trim",
)
f = request.files.get("files")
if not f:
return jsonify({"error": "No file uploaded."}), 400
start = (request.form.get("start") or "0").strip()
end = (request.form.get("end") or "").strip()
ext = f.filename.rsplit(".", 1)[-1].lower() if "." in f.filename else "mp4"
with tempfile.TemporaryDirectory() as tmp:
in_path = _save_upload(f, tmp)
out_path = os.path.join(tmp, f"output.{ext}")
args = ["-i", in_path, "-ss", start]
if end:
args += ["-to", end]
args += ["-c", "copy", out_path]
_, err = _run_ffmpeg(args)
if err:
# Re-encode fallback if stream copy fails (e.g. keyframe issues)
args = ["-i", in_path, "-ss", start]
if end:
args += ["-to", end]
args += [out_path]
_, err = _run_ffmpeg(args, timeout=600)
if err:
return jsonify({"error": err}), 400
with open(out_path, "rb") as fp:
data = fp.read()
base = f.filename.rsplit(".", 1)[0]
return send_file(
_bytes_io(data),
as_attachment=True,
download_name=f"{base}_trimmed.{ext}",
)
# ── Compress video ─────────────────────────────────────
@bp.route("/compress-video", methods=["GET", "POST"])
def compress_video():
if request.method == "GET":
return render_template(
"upload_tool.html",
title="Compress Video",
description="Reduce video file size by re-encoding with H.264 at a chosen quality level.",
notes=_ffmpeg_available_notes(),
endpoint="/media/compress-video",
accept=".mp4,.webm,.mkv,.mov,.avi,.flv",
multiple=False,
options=[
{
"name": "quality",
"label": "Quality (CRF)",
"type": "select",
"default": "28",
"choices": [
{"value": "23", "label": "High (23 larger, better)"},
{"value": "28", "label": "Medium (28 balanced)"},
{"value": "32", "label": "Low (32 smaller)"},
{"value": "36", "label": "Very low (36 smallest)"},
],
},
{
"name": "preset",
"label": "Encoding preset",
"type": "select",
"default": "medium",
"choices": [
{"value": "ultrafast", "label": "Ultrafast (largest)"},
{"value": "fast", "label": "Fast"},
{"value": "medium", "label": "Medium"},
{"value": "slow", "label": "Slow (smallest)"},
],
},
],
button_text="Compress",
)
f = request.files.get("files")
if not f:
return jsonify({"error": "No file uploaded."}), 400
crf = request.form.get("quality", "28")
preset = request.form.get("preset", "medium")
if crf not in ("23", "28", "32", "36"):
crf = "28"
if preset not in ("ultrafast", "fast", "medium", "slow"):
preset = "medium"
with tempfile.TemporaryDirectory() as tmp:
in_path = _save_upload(f, tmp)
out_path = os.path.join(tmp, "output.mp4")
args = ["-i", in_path, "-c:v", "libx264", "-crf", crf, "-preset", preset, "-c:a", "aac", "-b:a", "128k", out_path]
_, err = _run_ffmpeg(args, timeout=900)
if err:
return jsonify({"error": err}), 400
with open(out_path, "rb") as fp:
data = fp.read()
base = f.filename.rsplit(".", 1)[0]
return send_file(
_bytes_io(data),
mimetype="video/mp4",
as_attachment=True,
download_name=f"{base}_compressed.mp4",
)
# ── Video to GIF ───────────────────────────────────────
@bp.route("/video-to-gif", methods=["GET", "POST"])
def video_to_gif():
if request.method == "GET":
return render_template(
"upload_tool.html",
title="Video to GIF",
description="Convert a short video clip into an animated GIF.",
notes=_ffmpeg_available_notes()
+ "<p><strong>Tip:</strong> keep clips under ~10 seconds — GIFs are large.</p>",
endpoint="/media/video-to-gif",
accept=".mp4,.webm,.mkv,.mov,.avi",
multiple=False,
options=[
{"name": "fps", "label": "FPS", "type": "number", "default": 15, "min": 1, "max": 30},
{"name": "width", "label": "Width (px)", "type": "number", "default": 480, "min": 100, "max": 1920},
{"name": "start", "label": "Start (seconds or HH:MM:SS)", "type": "text", "default": "0"},
{"name": "duration", "label": "Duration (seconds, blank for all)", "type": "text", "default": "5"},
],
button_text="Convert",
)
f = request.files.get("files")
if not f:
return jsonify({"error": "No file uploaded."}), 400
try:
fps = max(1, min(30, int(request.form.get("fps", 15))))
width = max(100, min(1920, int(request.form.get("width", 480))))
except ValueError:
return jsonify({"error": "FPS and width must be integers."}), 400
start = (request.form.get("start") or "0").strip()
duration = (request.form.get("duration") or "").strip()
with tempfile.TemporaryDirectory() as tmp:
in_path = _save_upload(f, tmp)
out_path = os.path.join(tmp, "output.gif")
args = ["-ss", start]
if duration:
args += ["-t", duration]
args += [
"-i", in_path,
"-vf", f"fps={fps},scale={width}:-1:flags=lanczos",
out_path,
]
_, err = _run_ffmpeg(args, timeout=300)
if err:
return jsonify({"error": err}), 400
with open(out_path, "rb") as fp:
data = fp.read()
base = f.filename.rsplit(".", 1)[0]
return send_file(
_bytes_io(data),
mimetype="image/gif",
as_attachment=True,
download_name=f"{base}.gif",
)
# ── helpers ────────────────────────────────────────────
def _bytes_io(data: bytes):
import io as _io
return _io.BytesIO(data)
+60 -14
View File
@@ -268,26 +268,37 @@ def compress():
quality = request.form.get("quality", "medium")
image_quality = {"low": 40, "medium": 65, "high": 85}.get(quality, 65)
max_dim = {"low": 1200, "medium": 1800, "high": 2400}.get(quality, 1800)
from PIL import Image
doc = fitz.open(stream=files[0].read(), filetype="pdf")
processed_xrefs = set()
for page in doc:
images = page.get_images(full=True)
for img_info in images:
for img_info in page.get_images(full=True):
xref = img_info[0]
if xref in processed_xrefs:
continue
processed_xrefs.add(xref)
try:
base_image = doc.extract_image(xref)
if not base_image:
continue
img_bytes = base_image["image"]
from PIL import Image
pil_img = Image.open(io.BytesIO(img_bytes))
if pil_img.mode in ("RGBA", "P"):
pil_img = Image.open(io.BytesIO(base_image["image"]))
if pil_img.mode in ("RGBA", "LA", "P"):
pil_img = pil_img.convert("RGB")
elif pil_img.mode not in ("RGB", "L"):
pil_img = pil_img.convert("RGB")
if max(pil_img.size) > max_dim:
pil_img.thumbnail((max_dim, max_dim), Image.LANCZOS)
buf = io.BytesIO()
pil_img.save(buf, format="JPEG", quality=image_quality, optimize=True)
doc._deleteObject(xref)
page.insert_image(page.rect, stream=buf.getvalue())
# Replace image in-place — preserves original placement & size.
page.replace_image(xref, stream=buf.getvalue())
except Exception:
continue
@@ -334,21 +345,56 @@ def resize():
mode = request.form.get("mode", "scale")
doc = fitz.open(stream=files[0].read(), filetype="pdf")
new_doc = fitz.open()
if mode == "scale":
scale = float(request.form.get("scale", 100)) / 100.0
try:
scale = float(request.form.get("scale", 100)) / 100.0
except ValueError:
scale = 1.0
if scale <= 0:
doc.close()
new_doc.close()
return jsonify(error="Scale must be greater than 0."), 400
for page in doc:
r = page.rect
new_rect = fitz.Rect(0, 0, r.width * scale, r.height * scale)
page.set_mediabox(new_rect)
new_w = r.width * scale
new_h = r.height * scale
new_page = new_doc.new_page(width=new_w, height=new_h)
new_page.show_pdf_page(new_page.rect, doc, page.number,
rotate=page.rotation)
elif mode == "paper":
paper = request.form.get("paper", "a4")
w, h = PAPER_SIZES.get(paper, PAPER_SIZES["a4"])
target_w, target_h = PAPER_SIZES.get(paper, PAPER_SIZES["a4"])
for page in doc:
page.set_mediabox(fitz.Rect(0, 0, w, h))
r = page.rect
src_w, src_h = r.width, r.height
# Match target orientation to source orientation
if (src_w > src_h) != (target_w > target_h):
page_w, page_h = target_h, target_w
else:
page_w, page_h = target_w, target_h
# Fit source page into new page, preserving aspect ratio
fit = min(page_w / src_w, page_h / src_h)
content_w = src_w * fit
content_h = src_h * fit
x0 = (page_w - content_w) / 2
y0 = (page_h - content_h) / 2
new_page = new_doc.new_page(width=page_w, height=page_h)
new_page.show_pdf_page(
fitz.Rect(x0, y0, x0 + content_w, y0 + content_h),
doc, page.number, rotate=page.rotation
)
output = io.BytesIO()
doc.save(output)
new_doc.save(output, garbage=4, deflate=True)
new_doc.close()
doc.close()
output.seek(0)
+39 -1
View File
@@ -1,7 +1,10 @@
from flask import Blueprint, render_template
import hashlib
from flask import Blueprint, render_template, request, jsonify
bp = Blueprint("security", __name__)
HASH_ALGOS = ["md5", "sha1", "sha256", "sha512"]
@bp.route("/password-generator")
def password_generator():
@@ -11,3 +14,38 @@ def password_generator():
@bp.route("/hash-generator")
def hash_generator():
return render_template("tools/hash_generator.html")
@bp.route("/file-hash", methods=["GET", "POST"])
def file_hash():
if request.method == "GET":
return render_template(
"upload_tool.html",
title="File Hash",
description="Compute MD5, SHA-1, SHA-256, and SHA-512 hashes of an uploaded file.",
endpoint="/security/file-hash",
accept="*",
multiple=False,
button_text="Compute",
)
f = request.files.get("files")
if not f:
return jsonify({"error": "No file uploaded."}), 400
hashers = {name: hashlib.new(name) for name in HASH_ALGOS}
total = 0
chunk_size = 1024 * 1024 # 1 MB
while True:
chunk = f.stream.read(chunk_size)
if not chunk:
break
total += len(chunk)
for h in hashers.values():
h.update(chunk)
lines = [f"File: {f.filename}", f"Size: {total:,} bytes", ""]
for name in HASH_ALGOS:
lines.append(f"{name.upper():<8} {hashers[name].hexdigest()}")
return jsonify({"text": "\n".join(lines)})
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

+85
View File
@@ -0,0 +1,85 @@
{% extends "base.html" %}
{% block title %}Cron Parser - EveryTools{% endblock %}
{% block top_title %}Cron Parser{% endblock %}
{% block content %}
<div class="client-tool">
<div class="tool-header">
<h1>Cron Expression Parser</h1>
<p>Validate cron expressions and preview upcoming run times</p>
</div>
<div style="display:flex;gap:.5rem;margin-bottom:.6rem;flex-wrap:wrap;align-items:center">
<label>Expression:
<input type="text" id="cron-expr" value="*/5 * * * *" style="font-family:monospace;min-width:200px">
</label>
<label>Count:
<input type="number" id="cron-count" value="10" min="1" max="50" style="width:70px">
</label>
<button class="btn btn-primary" onclick="parseCron()"><i class="bi bi-play-circle"></i> Parse</button>
</div>
<div style="display:flex;gap:.4rem;flex-wrap:wrap;margin-bottom:.6rem">
<button class="btn btn-small" onclick="setExpr('* * * * *')">Every minute</button>
<button class="btn btn-small" onclick="setExpr('*/5 * * * *')">Every 5 min</button>
<button class="btn btn-small" onclick="setExpr('0 * * * *')">Hourly</button>
<button class="btn btn-small" onclick="setExpr('0 0 * * *')">Daily midnight</button>
<button class="btn btn-small" onclick="setExpr('0 9 * * 1-5')">Weekdays 9am</button>
<button class="btn btn-small" onclick="setExpr('0 0 1 * *')">Monthly 1st</button>
<button class="btn btn-small" onclick="setExpr('0 0 * * 0')">Weekly Sunday</button>
</div>
<div id="cron-status" style="margin:.5rem 0;font-size:.9rem"></div>
<div class="split-pane">
<div class="pane">
<div class="pane-header"><span>Description</span></div>
<div class="pane-body"><pre id="cron-desc"></pre></div>
</div>
<div class="pane">
<div class="pane-header"><span>Next runs</span></div>
<div class="pane-body"><pre id="cron-next"></pre></div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function setExpr(e) {
document.getElementById("cron-expr").value = e;
parseCron();
}
async function parseCron() {
const expr = document.getElementById("cron-expr").value;
const count = document.getElementById("cron-count").value;
const status = document.getElementById("cron-status");
if (!expr.trim()) { status.textContent = "Enter a cron expression."; return; }
const fd = new FormData();
fd.append("expr", expr);
fd.append("count", count);
status.textContent = "Parsing...";
try {
const r = await fetch("/dev/cron", { method: "POST", body: fd });
const j = await r.json();
if (j.error) {
status.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle"></i> ${j.error}</span>`;
document.getElementById("cron-desc").textContent = "";
document.getElementById("cron-next").textContent = "";
} else {
status.innerHTML = '<span style="color:var(--success)"><i class="bi bi-check-circle"></i> Valid expression</span>';
document.getElementById("cron-desc").textContent = j.description;
document.getElementById("cron-next").textContent = j.next.join("\n");
}
} catch (e) {
status.innerHTML = `<span style="color:var(--danger)">Network error: ${e.message}</span>`;
}
}
parseCron();
</script>
{% endblock %}
+92
View File
@@ -0,0 +1,92 @@
{% extends "base.html" %}
{% block title %}CSS Formatter - EveryTools{% endblock %}
{% block top_title %}CSS Formatter{% endblock %}
{% block content %}
<div class="client-tool">
<div class="tool-header">
<h1>CSS Formatter</h1>
<p>Beautify or minify CSS</p>
</div>
<div class="split-pane">
<div class="pane">
<div class="pane-header">
<span>Input</span>
<div>
<button class="btn btn-small" onclick="formatCSS()">Beautify</button>
<button class="btn btn-small" onclick="minifyCSS()">Minify</button>
</div>
</div>
<div class="pane-body">
<textarea id="css-input" placeholder=".example { color: red; background: #fff; }"></textarea>
</div>
</div>
<div class="pane">
<div class="pane-header">
<span>Output</span>
<button class="btn btn-small" onclick="copyOutput()"><i class="bi bi-clipboard"></i> Copy</button>
</div>
<div class="pane-body">
<pre id="css-output"></pre>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function formatCSS() {
let src = document.getElementById("css-input").value;
src = src.replace(/\/\*[\s\S]*?\*\//g, m => "\u0000COMMENT" + btoa(unescape(encodeURIComponent(m))) + "\u0000");
const out = [];
let depth = 0;
let i = 0;
let buf = "";
const pad = () => " ".repeat(depth);
while (i < src.length) {
const ch = src[i];
if (ch === "{") {
out.push(pad() + buf.trim() + " {");
depth++;
buf = "";
} else if (ch === "}") {
if (buf.trim()) out.push(pad() + buf.trim() + (buf.trim().endsWith(";") ? "" : ";"));
depth = Math.max(0, depth - 1);
out.push(pad() + "}");
buf = "";
} else if (ch === ";") {
const line = buf.trim();
if (line) out.push(pad() + line + ";");
buf = "";
} else {
buf += ch;
}
i++;
}
if (buf.trim()) out.push(pad() + buf.trim());
let result = out.join("\n").replace(/\u0000COMMENT([A-Za-z0-9+/=]+)\u0000/g, (_, b) => {
try { return decodeURIComponent(escape(atob(b))); } catch (e) { return ""; }
});
document.getElementById("css-output").textContent = result;
}
function minifyCSS() {
const src = document.getElementById("css-input").value;
const out = src
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\s*([{}:;,>~+])\s*/g, "$1")
.replace(/;}/g, "}")
.replace(/\s+/g, " ")
.trim();
document.getElementById("css-output").textContent = out;
}
function copyOutput() {
navigator.clipboard.writeText(document.getElementById("css-output").textContent);
}
</script>
{% endblock %}
+133
View File
@@ -0,0 +1,133 @@
{% extends "base.html" %}
{% block title %}HTML Formatter - EveryTools{% endblock %}
{% block top_title %}HTML Formatter{% endblock %}
{% block content %}
<div class="client-tool">
<div class="tool-header">
<h1>HTML Formatter</h1>
<p>Beautify or minify HTML source</p>
</div>
<div class="split-pane">
<div class="pane">
<div class="pane-header">
<span>Input</span>
<div>
<button class="btn btn-small" onclick="formatHTML()">Beautify</button>
<button class="btn btn-small" onclick="minifyHTML()">Minify</button>
</div>
</div>
<div class="pane-body">
<textarea id="html-input" placeholder="<div><p>Hello</p></div>"></textarea>
</div>
</div>
<div class="pane">
<div class="pane-header">
<span>Output</span>
<button class="btn btn-small" onclick="copyOutput()"><i class="bi bi-clipboard"></i> Copy</button>
</div>
<div class="pane-body">
<pre id="html-output"></pre>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const VOID_TAGS = new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);
const INLINE_TAGS = new Set(["a","abbr","b","bdo","br","cite","code","em","i","img","input","kbd","label","mark","q","s","small","span","strong","sub","sup","time","u","var"]);
function tokenize(src) {
const tokens = [];
let i = 0;
while (i < src.length) {
if (src[i] === "<") {
const end = src.indexOf(">", i);
if (end === -1) { tokens.push({type: "text", value: src.slice(i)}); break; }
const tag = src.slice(i, end + 1);
if (tag.startsWith("<!--")) {
const c = src.indexOf("-->", i);
tokens.push({type: "comment", value: src.slice(i, c === -1 ? src.length : c + 3)});
i = c === -1 ? src.length : c + 3;
} else if (tag.startsWith("<!")) {
tokens.push({type: "doctype", value: tag});
i = end + 1;
} else if (tag.startsWith("</")) {
tokens.push({type: "close", value: tag, name: tag.slice(2, -1).trim().toLowerCase()});
i = end + 1;
} else {
const name = tag.slice(1).split(/[\s>/]/)[0].toLowerCase();
const selfClosing = tag.endsWith("/>") || VOID_TAGS.has(name);
tokens.push({type: selfClosing ? "void" : "open", value: tag, name});
i = end + 1;
if (["script","style","pre","textarea"].includes(name) && !selfClosing) {
const closeRe = new RegExp(`</${name}\\s*>`, "i");
const m = src.slice(i).match(closeRe);
if (m) {
tokens.push({type: "raw", value: src.slice(i, i + m.index)});
tokens.push({type: "close", value: m[0], name});
i += m.index + m[0].length;
}
}
}
} else {
const next = src.indexOf("<", i);
const text = src.slice(i, next === -1 ? src.length : next);
if (text.trim()) tokens.push({type: "text", value: text.trim()});
i = next === -1 ? src.length : next;
}
}
return tokens;
}
function formatHTML() {
const src = document.getElementById("html-input").value;
const tokens = tokenize(src);
const out = [];
let depth = 0;
const pad = () => " ".repeat(depth);
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i];
const next = tokens[i + 1];
if (t.type === "open") {
// Inline tag with just text inside, keep on one line
if (INLINE_TAGS.has(t.name) && next && next.type === "text" &&
tokens[i + 2] && tokens[i + 2].type === "close" && tokens[i + 2].name === t.name) {
out.push(pad() + t.value + next.value + tokens[i + 2].value);
i += 2;
} else {
out.push(pad() + t.value);
depth++;
}
} else if (t.type === "close") {
depth = Math.max(0, depth - 1);
out.push(pad() + t.value);
} else if (t.type === "void" || t.type === "doctype" || t.type === "comment") {
out.push(pad() + t.value);
} else if (t.type === "text") {
out.push(pad() + t.value);
} else if (t.type === "raw") {
out.push(t.value);
}
}
document.getElementById("html-output").textContent = out.join("\n");
}
function minifyHTML() {
const src = document.getElementById("html-input").value;
const out = src
.replace(/<!--[\s\S]*?-->/g, "")
.replace(/>\s+</g, "><")
.replace(/\s{2,}/g, " ")
.trim();
document.getElementById("html-output").textContent = out;
}
function copyOutput() {
navigator.clipboard.writeText(document.getElementById("html-output").textContent);
}
</script>
{% endblock %}
+147
View File
@@ -0,0 +1,147 @@
{% extends "base.html" %}
{% block title %}JS Formatter - EveryTools{% endblock %}
{% block top_title %}JS Formatter{% endblock %}
{% block content %}
<div class="client-tool">
<div class="tool-header">
<h1>JavaScript Formatter</h1>
<p>Basic beautification and minification. For complex code, use a full tool like Prettier.</p>
</div>
<div class="split-pane">
<div class="pane">
<div class="pane-header">
<span>Input</span>
<div>
<button class="btn btn-small" onclick="formatJS()">Beautify</button>
<button class="btn btn-small" onclick="minifyJS()">Minify</button>
</div>
</div>
<div class="pane-body">
<textarea id="js-input" placeholder="function hello(){console.log('hi');}"></textarea>
</div>
</div>
<div class="pane">
<div class="pane-header">
<span>Output</span>
<button class="btn btn-small" onclick="copyOutput()"><i class="bi bi-clipboard"></i> Copy</button>
</div>
<div class="pane-body">
<pre id="js-output"></pre>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Stash strings, regex, and comments so we don't mangle them.
function protect(src) {
const stash = [];
let i = 0;
let out = "";
const push = v => { const k = `\u0000__${stash.length}__\u0000`; stash.push(v); return k; };
while (i < src.length) {
const c = src[i];
if (c === "/" && src[i+1] === "/") {
const end = src.indexOf("\n", i);
const seg = src.slice(i, end === -1 ? src.length : end);
out += push(seg);
i = end === -1 ? src.length : end;
} else if (c === "/" && src[i+1] === "*") {
const end = src.indexOf("*/", i);
const seg = src.slice(i, end === -1 ? src.length : end + 2);
out += push(seg);
i = end === -1 ? src.length : end + 2;
} else if (c === '"' || c === "'" || c === "`") {
let j = i + 1;
while (j < src.length && src[j] !== c) {
if (src[j] === "\\") j += 2;
else j++;
}
out += push(src.slice(i, j + 1));
i = j + 1;
} else {
out += c;
i++;
}
}
return { code: out, stash };
}
function restore(code, stash) {
return code.replace(/\u0000__(\d+)__\u0000/g, (_, n) => stash[+n]);
}
function formatJS() {
const src = document.getElementById("js-input").value;
const { code, stash } = protect(src);
let out = "";
let depth = 0;
let inParens = 0;
const pad = () => " ".repeat(depth);
let lineStarted = true;
for (let i = 0; i < code.length; i++) {
const c = code[i];
if (c === "{") {
out = out.trimEnd() + " {\n";
depth++;
lineStarted = true;
i = skipSpace(code, i + 1) - 1;
} else if (c === "}") {
depth = Math.max(0, depth - 1);
out = out.trimEnd() + "\n" + pad() + "}";
lineStarted = false;
// Newline after } unless followed by comma/semicolon/paren
const nx = code[skipSpace(code, i + 1)];
if (nx && ",;)]".indexOf(nx) === -1) { out += "\n"; lineStarted = true; }
} else if (c === ";") {
out += ";\n";
lineStarted = true;
i = skipSpace(code, i + 1) - 1;
} else if (c === "(") {
inParens++;
out += c;
} else if (c === ")") {
inParens = Math.max(0, inParens - 1);
out += c;
} else if (c === "\n" || c === "\r") {
if (!lineStarted) { out += "\n"; lineStarted = true; }
} else if (/\s/.test(c)) {
if (lineStarted) continue;
out += " ";
} else {
if (lineStarted) { out += pad(); lineStarted = false; }
out += c;
}
}
out = out.replace(/\n{3,}/g, "\n\n");
document.getElementById("js-output").textContent = restore(out, stash).trim();
}
function skipSpace(s, i) {
while (i < s.length && /\s/.test(s[i])) i++;
return i;
}
function minifyJS() {
const src = document.getElementById("js-input").value;
const { code, stash } = protect(src);
const minified = code
.replace(/\n/g, " ")
.replace(/\s*([{}();,:=<>+\-*/%!&|?])\s*/g, "$1")
.replace(/\s+/g, " ")
.trim();
document.getElementById("js-output").textContent = restore(minified, stash);
}
function copyOutput() {
navigator.clipboard.writeText(document.getElementById("js-output").textContent);
}
</script>
{% endblock %}
+108
View File
@@ -0,0 +1,108 @@
{% extends "base.html" %}
{% block title %}JSONPath Tester - EveryTools{% endblock %}
{% block top_title %}JSONPath Tester{% endblock %}
{% block content %}
<div class="client-tool">
<div class="tool-header">
<h1>JSONPath Tester</h1>
<p>Query JSON data using JSONPath expressions</p>
</div>
<div style="display:flex;gap:.5rem;margin-bottom:.6rem;flex-wrap:wrap;align-items:center">
<label style="flex:1;min-width:240px">Path:
<input type="text" id="jp-path" value="$.store.book[*].title" style="font-family:monospace;width:100%">
</label>
<button class="btn btn-primary" onclick="runPath()"><i class="bi bi-play-circle"></i> Evaluate</button>
</div>
<div style="display:flex;gap:.4rem;flex-wrap:wrap;margin-bottom:.6rem">
<button class="btn btn-small" onclick="setExample()">Load example</button>
<button class="btn btn-small" onclick="setPath('$..author')">All authors</button>
<button class="btn btn-small" onclick="setPath('$.store.book[?(@.price < 10)]')">Cheap books</button>
<button class="btn btn-small" onclick="setPath('$..book[0,1]')">First two books</button>
<button class="btn btn-small" onclick="setPath('$..*')">Everything</button>
</div>
<div class="split-pane">
<div class="pane">
<div class="pane-header"><span>JSON Input</span></div>
<div class="pane-body">
<textarea id="jp-input" placeholder='{"key": "value"}'></textarea>
</div>
</div>
<div class="pane">
<div class="pane-header">
<span>Matches</span>
<button class="btn btn-small" onclick="copyOutput()"><i class="bi bi-clipboard"></i> Copy</button>
</div>
<div class="pane-body"><pre id="jp-output"></pre></div>
</div>
</div>
<div id="jp-status" style="margin-top:.5rem;font-size:.85rem"></div>
</div>
{% endblock %}
{% block scripts %}
<script>
const EXAMPLE = {
store: {
book: [
{category: "fiction", author: "J.R.R. Tolkien", title: "The Hobbit", price: 14.99},
{category: "fiction", author: "George Orwell", title: "1984", price: 9.99},
{category: "reference", author: "Nigel Rees", title: "Sayings of the Century", price: 8.95},
{category: "fiction", author: "Evelyn Waugh", title: "Sword of Honour", price: 12.99}
],
bicycle: {color: "red", price: 19.95}
}
};
function setExample() {
document.getElementById("jp-input").value = JSON.stringify(EXAMPLE, null, 2);
}
function setPath(p) {
document.getElementById("jp-path").value = p;
runPath();
}
async function runPath() {
const data = document.getElementById("jp-input").value;
const path = document.getElementById("jp-path").value;
const status = document.getElementById("jp-status");
if (!data.trim()) { status.textContent = "Provide JSON data."; return; }
if (!path.trim()) { status.textContent = "Provide a JSONPath expression."; return; }
try { JSON.parse(data); } catch (e) {
status.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle"></i> Invalid JSON: ${e.message}</span>`;
return;
}
const fd = new FormData();
fd.append("data", data);
fd.append("path", path);
status.textContent = "Evaluating...";
try {
const r = await fetch("/dev/jsonpath", { method: "POST", body: fd });
const j = await r.json();
if (j.error) {
status.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle"></i> ${j.error}</span>`;
document.getElementById("jp-output").textContent = "";
} else {
document.getElementById("jp-output").textContent = JSON.stringify(j.matches, null, 2);
status.innerHTML = `<span style="color:var(--success)"><i class="bi bi-check-circle"></i> ${j.count} match${j.count === 1 ? "" : "es"}</span>`;
}
} catch (e) {
status.innerHTML = `<span style="color:var(--danger)">Network error: ${e.message}</span>`;
}
}
function copyOutput() {
navigator.clipboard.writeText(document.getElementById("jp-output").textContent);
}
setExample();
</script>
{% endblock %}
+106
View File
@@ -0,0 +1,106 @@
{% extends "base.html" %}
{% block title %}JWT Decoder - EveryTools{% endblock %}
{% block top_title %}JWT Decoder{% endblock %}
{% block content %}
<div class="client-tool">
<div class="tool-header">
<h1>JWT Decoder</h1>
<p>Decode header and payload of a JSON Web Token. Signatures are NOT verified — do not trust decoded content without verification.</p>
</div>
<div class="form-group">
<label>Paste JWT</label>
<textarea id="jwt-input" class="text-input" rows="4"
placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
oninput="decode()"></textarea>
</div>
<div id="jwt-status" style="margin-bottom:1rem;font-size:.9rem"></div>
<div class="split-pane">
<div class="pane">
<div class="pane-header">
<span>Header <span style="font-weight:400;color:var(--text-light)">(algorithm &amp; type)</span></span>
<button class="btn btn-small" onclick="copyField('jwt-header')"><i class="bi bi-clipboard"></i></button>
</div>
<div class="pane-body"><pre id="jwt-header"></pre></div>
</div>
<div class="pane">
<div class="pane-header">
<span>Payload <span style="font-weight:400;color:var(--text-light)">(claims)</span></span>
<button class="btn btn-small" onclick="copyField('jwt-payload')"><i class="bi bi-clipboard"></i></button>
</div>
<div class="pane-body"><pre id="jwt-payload"></pre></div>
</div>
</div>
<div class="pane" style="margin-top:.8rem">
<div class="pane-header">
<span>Signature</span>
<button class="btn btn-small" onclick="copyField('jwt-sig')"><i class="bi bi-clipboard"></i></button>
</div>
<div class="pane-body"><pre id="jwt-sig" style="word-break:break-all"></pre></div>
</div>
<div id="jwt-claims" style="margin-top:1rem;font-size:.9rem;color:var(--text-light)"></div>
</div>
{% endblock %}
{% block scripts %}
<script>
function b64urlDecode(s) {
s = s.replace(/-/g, "+").replace(/_/g, "/");
while (s.length % 4) s += "=";
const bin = atob(s);
const bytes = new Uint8Array([...bin].map(c => c.charCodeAt(0)));
return new TextDecoder("utf-8").decode(bytes);
}
function decode() {
const val = document.getElementById("jwt-input").value.trim();
const status = document.getElementById("jwt-status");
const claims = document.getElementById("jwt-claims");
["jwt-header","jwt-payload","jwt-sig"].forEach(i => document.getElementById(i).textContent = "");
claims.innerHTML = "";
if (!val) { status.textContent = ""; return; }
const parts = val.split(".");
if (parts.length !== 3) {
status.innerHTML = '<span style="color:var(--danger)"><i class="bi bi-x-circle"></i> Invalid JWT: expected 3 dot-separated parts</span>';
return;
}
try {
const header = JSON.parse(b64urlDecode(parts[0]));
const payload = JSON.parse(b64urlDecode(parts[1]));
document.getElementById("jwt-header").textContent = JSON.stringify(header, null, 2);
document.getElementById("jwt-payload").textContent = JSON.stringify(payload, null, 2);
document.getElementById("jwt-sig").textContent = parts[2];
status.innerHTML = '<span style="color:var(--success)"><i class="bi bi-check-circle"></i> Decoded successfully (signature not verified)</span>';
const notes = [];
const now = Math.floor(Date.now() / 1000);
if (payload.exp) {
const d = new Date(payload.exp * 1000);
const expired = payload.exp < now;
notes.push(`<strong>exp:</strong> ${d.toLocaleString()} ${expired ? '<span style="color:var(--danger)">(EXPIRED)</span>' : '<span style="color:var(--success)">(valid)</span>'}`);
}
if (payload.iat) notes.push(`<strong>iat:</strong> ${new Date(payload.iat * 1000).toLocaleString()}`);
if (payload.nbf) notes.push(`<strong>nbf:</strong> ${new Date(payload.nbf * 1000).toLocaleString()}`);
if (payload.iss) notes.push(`<strong>iss:</strong> ${payload.iss}`);
if (payload.aud) notes.push(`<strong>aud:</strong> ${Array.isArray(payload.aud) ? payload.aud.join(", ") : payload.aud}`);
if (payload.sub) notes.push(`<strong>sub:</strong> ${payload.sub}`);
if (notes.length) claims.innerHTML = notes.join(" &middot; ");
} catch (e) {
status.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle"></i> Decode error: ${e.message}</span>`;
}
}
function copyField(id) {
navigator.clipboard.writeText(document.getElementById(id).textContent);
}
</script>
{% endblock %}
+80
View File
@@ -0,0 +1,80 @@
{% extends "base.html" %}
{% block title %}SQL Formatter - EveryTools{% endblock %}
{% block top_title %}SQL Formatter{% endblock %}
{% block content %}
<div class="client-tool">
<div class="tool-header">
<h1>SQL Formatter</h1>
<p>Format SQL statements with proper indentation and keyword casing</p>
</div>
<div style="display:flex;gap:.5rem;margin-bottom:.6rem;flex-wrap:wrap;align-items:center">
<label>Keyword case:
<select id="sql-keyword">
<option value="upper" selected>UPPERCASE</option>
<option value="lower">lowercase</option>
<option value="capitalize">Capitalize</option>
</select>
</label>
<label>Indent:
<select id="sql-indent">
<option value="2" selected>2 spaces</option>
<option value="4">4 spaces</option>
<option value="tab">tab</option>
</select>
</label>
<button class="btn btn-primary" onclick="formatSQL()"><i class="bi bi-magic"></i> Format</button>
</div>
<div class="split-pane">
<div class="pane">
<div class="pane-header"><span>Input</span></div>
<div class="pane-body">
<textarea id="sql-input" placeholder="select * from users where id=1"></textarea>
</div>
</div>
<div class="pane">
<div class="pane-header">
<span>Output</span>
<button class="btn btn-small" onclick="copyOutput()"><i class="bi bi-clipboard"></i> Copy</button>
</div>
<div class="pane-body"><pre id="sql-output"></pre></div>
</div>
</div>
<div id="sql-status" style="margin-top:.5rem;font-size:.85rem"></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function formatSQL() {
const input = document.getElementById("sql-input").value;
const status = document.getElementById("sql-status");
if (!input.trim()) { status.textContent = "Enter some SQL."; return; }
const fd = new FormData();
fd.append("sql", input);
fd.append("keyword_case", document.getElementById("sql-keyword").value);
fd.append("indent", document.getElementById("sql-indent").value);
status.textContent = "Formatting...";
try {
const r = await fetch("/dev/sql-format", { method: "POST", body: fd });
const j = await r.json();
if (j.error) {
status.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle"></i> ${j.error}</span>`;
} else {
document.getElementById("sql-output").textContent = j.text;
status.innerHTML = '<span style="color:var(--success)"><i class="bi bi-check-circle"></i> Formatted</span>';
}
} catch (e) {
status.innerHTML = `<span style="color:var(--danger)">Network error: ${e.message}</span>`;
}
}
function copyOutput() {
navigator.clipboard.writeText(document.getElementById("sql-output").textContent);
}
</script>
{% endblock %}
+89
View File
@@ -0,0 +1,89 @@
{% extends "base.html" %}
{% block title %}User-Agent Parser - EveryTools{% endblock %}
{% block top_title %}User-Agent Parser{% endblock %}
{% block content %}
<div class="client-tool">
<div class="tool-header">
<h1>User-Agent Parser</h1>
<p>Extract browser, OS, and device info from a User-Agent string.</p>
</div>
<div class="form-group">
<label>User-Agent string <button class="btn btn-small" onclick="useMine()" style="margin-left:.5rem">Use mine</button></label>
<textarea id="ua-input" class="text-input" rows="3"
placeholder="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..."
oninput="parseUA()"></textarea>
</div>
<div id="ua-results" style="display:grid;grid-template-columns:1fr 1fr;gap:.8rem"></div>
</div>
{% endblock %}
{% block scripts %}
<script>
function row(label, val) {
return `<div style="padding:.35rem .6rem;border-bottom:1px solid var(--border)">
<div style="font-size:.75rem;color:var(--text-light);text-transform:uppercase;letter-spacing:.03em">${label}</div>
<div style="font-weight:500">${val || "—"}</div>
</div>`;
}
function parseUA() {
const ua = document.getElementById("ua-input").value.trim();
const out = document.getElementById("ua-results");
if (!ua) { out.innerHTML = ""; return; }
let browser = "Unknown", browserVer = "";
let os = "Unknown", osVer = "";
let device = "Desktop";
let engine = "Unknown";
// Browser — order matters (more specific first)
const b = [
["Edg/", "Edge"],
["OPR/|Opera/", "Opera"],
["Firefox/", "Firefox"],
["Chrome/", "Chrome"],
["Safari/", "Safari"],
["MSIE |Trident/", "Internet Explorer"],
];
for (const [pat, name] of b) {
const m = ua.match(new RegExp(`(?:${pat})([\\d.]+)`));
if (m) { browser = name; browserVer = m[1]; break; }
}
// OS
if (/Windows NT 10\.0/.test(ua)) { os = "Windows"; osVer = "10 / 11"; }
else if (/Windows NT 6\.3/.test(ua)) { os = "Windows"; osVer = "8.1"; }
else if (/Windows NT 6\.1/.test(ua)) { os = "Windows"; osVer = "7"; }
else if (/Mac OS X ([\d_.]+)/.test(ua)) { os = "macOS"; osVer = RegExp.$1.replace(/_/g, "."); }
else if (/Android ([\d.]+)/.test(ua)) { os = "Android"; osVer = RegExp.$1; device = "Mobile"; }
else if (/iPhone OS ([\d_]+)/.test(ua)) { os = "iOS"; osVer = RegExp.$1.replace(/_/g, "."); device = "Mobile"; }
else if (/iPad; CPU OS ([\d_]+)/.test(ua)) { os = "iPadOS"; osVer = RegExp.$1.replace(/_/g, "."); device = "Tablet"; }
else if (/CrOS/.test(ua)) { os = "ChromeOS"; }
else if (/Linux/.test(ua)) { os = "Linux"; }
// Device refine
if (/Mobile|iPhone|Android.*Mobile/.test(ua)) device = "Mobile";
else if (/iPad|Tablet/.test(ua)) device = "Tablet";
else if (/bot|crawler|spider|slurp/i.test(ua)) device = "Bot / Crawler";
// Engine
if (/Gecko\//.test(ua) && !/like Gecko/.test(ua)) engine = "Gecko";
else if (/AppleWebKit\//.test(ua)) engine = /Chrome|Chromium|Edg/.test(ua) ? "Blink" : "WebKit";
else if (/Trident\//.test(ua)) engine = "Trident";
out.innerHTML =
row("Browser", browser + (browserVer ? " " + browserVer : "")) +
row("OS", os + (osVer ? " " + osVer : "")) +
row("Device Type", device) +
row("Engine", engine);
}
function useMine() {
document.getElementById("ua-input").value = navigator.userAgent;
parseUA();
}
</script>
{% endblock %}
+81
View File
@@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block title %}UUID Generator - EveryTools{% endblock %}
{% block top_title %}UUID Generator{% endblock %}
{% block content %}
<div class="client-tool" style="max-width:640px">
<div class="tool-header">
<h1>UUID Generator</h1>
<p>Generate v4 (random) and nil UUIDs. Optionally generate many at once.</p>
</div>
<div class="form-group" style="display:grid;grid-template-columns:1fr 150px;gap:.6rem;align-items:end">
<div>
<label>How many?</label>
<input type="number" id="uuid-count" value="1" min="1" max="1000">
</div>
<button class="btn btn-primary" onclick="generateUUIDs()"><i class="bi bi-arrow-repeat"></i> Generate</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem;margin-bottom:1rem">
<label class="checkbox-label"><input type="checkbox" id="uuid-upper" onchange="generateUUIDs()"><span>Uppercase</span></label>
<label class="checkbox-label"><input type="checkbox" id="uuid-braces" onchange="generateUUIDs()"><span>Wrap in braces { }</span></label>
<label class="checkbox-label"><input type="checkbox" id="uuid-nodash" onchange="generateUUIDs()"><span>Remove dashes</span></label>
</div>
<div class="pane">
<div class="pane-header">
<span>Output</span>
<button class="btn btn-small" onclick="copyOutput()"><i class="bi bi-clipboard"></i> Copy</button>
</div>
<div class="pane-body">
<pre id="uuid-output" style="white-space:pre-wrap;word-break:break-all"></pre>
</div>
</div>
<div style="margin-top:1rem;font-size:.85rem;color:var(--text-light)">
<p><strong>Special values:</strong>
<button class="btn btn-small" onclick="showNil()">Nil UUID</button>
<button class="btn btn-small" onclick="showMax()">Max UUID</button>
</p>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function uuidv4() {
if (crypto.randomUUID) return crypto.randomUUID();
const b = crypto.getRandomValues(new Uint8Array(16));
b[6] = (b[6] & 0x0f) | 0x40;
b[8] = (b[8] & 0x3f) | 0x80;
const h = [...b].map(x => x.toString(16).padStart(2, "0")).join("");
return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`;
}
function format(u) {
if (document.getElementById("uuid-nodash").checked) u = u.replace(/-/g, "");
if (document.getElementById("uuid-upper").checked) u = u.toUpperCase();
if (document.getElementById("uuid-braces").checked) u = "{" + u + "}";
return u;
}
function generateUUIDs() {
const n = Math.min(1000, Math.max(1, parseInt(document.getElementById("uuid-count").value) || 1));
const out = [];
for (let i = 0; i < n; i++) out.push(format(uuidv4()));
document.getElementById("uuid-output").textContent = out.join("\n");
}
function showNil() {
document.getElementById("uuid-output").textContent = format("00000000-0000-0000-0000-000000000000");
}
function showMax() {
document.getElementById("uuid-output").textContent = format("ffffffff-ffff-ffff-ffff-ffffffffffff");
}
function copyOutput() {
navigator.clipboard.writeText(document.getElementById("uuid-output").textContent);
}
generateUUIDs();
</script>
{% endblock %}
+99
View File
@@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block title %}XML Formatter - EveryTools{% endblock %}
{% block top_title %}XML Formatter{% endblock %}
{% block content %}
<div class="client-tool">
<div class="tool-header">
<h1>XML Formatter</h1>
<p>Format, validate, and minify XML data</p>
</div>
<div class="split-pane">
<div class="pane">
<div class="pane-header">
<span>Input</span>
<div>
<button class="btn btn-small" onclick="formatXML()">Format</button>
<button class="btn btn-small" onclick="minifyXML()">Minify</button>
</div>
</div>
<div class="pane-body">
<textarea id="xml-input" placeholder="<root><item>value</item></root>"></textarea>
</div>
</div>
<div class="pane">
<div class="pane-header">
<span>Output</span>
<button class="btn btn-small" onclick="copyOutput()"><i class="bi bi-clipboard"></i> Copy</button>
</div>
<div class="pane-body">
<pre id="xml-output"></pre>
</div>
</div>
</div>
<div id="xml-status" style="margin-top:.5rem;font-size:.85rem"></div>
</div>
{% endblock %}
{% block scripts %}
<script>
function parseXML(src) {
const doc = new DOMParser().parseFromString(src, "application/xml");
const err = doc.querySelector("parsererror");
if (err) throw new Error(err.textContent.split("\n")[0]);
return doc;
}
function pretty(node, indent) {
const pad = " ".repeat(indent);
if (node.nodeType === 3) {
const t = node.nodeValue.trim();
return t ? pad + t : "";
}
if (node.nodeType === 8) return pad + "<!--" + node.nodeValue + "-->";
if (node.nodeType !== 1) return "";
const attrs = [...node.attributes].map(a => ` ${a.name}="${a.value.replace(/"/g,'&quot;')}"`).join("");
const name = node.nodeName;
const kids = [...node.childNodes].filter(n => !(n.nodeType === 3 && !n.nodeValue.trim()));
if (!kids.length) return `${pad}<${name}${attrs}/>`;
if (kids.length === 1 && kids[0].nodeType === 3) {
return `${pad}<${name}${attrs}>${kids[0].nodeValue.trim()}</${name}>`;
}
const body = kids.map(k => pretty(k, indent + 1)).filter(Boolean).join("\n");
return `${pad}<${name}${attrs}>\n${body}\n${pad}</${name}>`;
}
function formatXML() {
const input = document.getElementById("xml-input").value;
const status = document.getElementById("xml-status");
try {
const doc = parseXML(input);
const result = pretty(doc.documentElement, 0);
const decl = input.trim().startsWith("<?xml") ? input.trim().match(/^<\?xml[^?]*\?>/)[0] + "\n" : "";
document.getElementById("xml-output").textContent = decl + result;
status.innerHTML = '<span style="color:var(--success)"><i class="bi bi-check-circle"></i> Valid XML</span>';
} catch (e) {
status.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle"></i> ${e.message}</span>`;
}
}
function minifyXML() {
const input = document.getElementById("xml-input").value;
const status = document.getElementById("xml-status");
try {
parseXML(input);
const out = input.replace(/>\s+</g, "><").replace(/\s+/g, " ").trim();
document.getElementById("xml-output").textContent = out;
status.innerHTML = '<span style="color:var(--success)"><i class="bi bi-check-circle"></i> Valid XML (minified)</span>';
} catch (e) {
status.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle"></i> ${e.message}</span>`;
}
}
function copyOutput() {
navigator.clipboard.writeText(document.getElementById("xml-output").textContent);
}
</script>
{% endblock %}
+1 -1
View File
@@ -32,8 +32,8 @@
<span>or click to browse</span>
{% if accept %}<small>Accepted: {{ accept }}</small>{% endif %}
</div>
<div class="file-list" id="file-list"></div>
</div>
<div class="file-list" id="file-list"></div>
{% endif %}
{% if options %}