Files
toolvana/app/Views/tool.php
T
deepseek beaf0e1f37 Toolvana: production-ready media tools platform (CodeIgniter 4)
- 134-tool registry with programmatic SEO (unique titles/H1/descriptions,
  JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter)
- DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD),
  Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG)
- Security: SSRF guard, MIME validation, rate limits, API-key auth,
  bcrypt admin login, security headers
- Admin panel: dashboard, tools/categories/guides CRUD, SEO audit,
  analytics, job inspector with retry, system health, feature flags
- Docker deployment (nginx + web/api FPM pools + scalable workers),
  PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
2026-08-23 07:10:30 +00:00

249 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Individual tool page. Vars:
* $tool registry row, $category, $related, $conversions, $guides, $capabilities
*/
$kind = $tool['kind'];
$isYt = str_starts_with((string) $tool['operation'], 'yt_');
$ytEnabled = (bool) ($capabilities['yt_dlp'] ?? false);
$binariesOk = true;
foreach (($capabilities['binaries'] ?? []) as $ok) {
$binariesOk = $binariesOk && $ok;
}
$pageScript = match (true) {
$kind === 'text' => 'text-tool',
$kind === 'url' => 'url-tool',
default => 'upload-tool',
};
?>
<div class="wrap">
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<?php if ($category !== null): ?><li><a href="/<?= esc($category['slug']) ?>"><?= esc($category['name']) ?></a></li><?php endif ?>
<li aria-current="page"><?= esc($tool['name']) ?></li>
</ol>
</nav>
<header class="tool-head">
<h1><?= esc($tool['h1'] ?: $tool['name']) ?></h1>
<p class="hint" style="font-size:.98rem;max-width:70ch"><?= esc($tool['intro']) ?></p>
</header>
<section class="tool-panel" id="tool-panel" data-tool="<?= esc($tool['slug']) ?>" data-operation="<?= esc((string) $tool['operation']) ?>" data-kind="<?= esc($kind) ?>">
<?php if ($isYt && ! $ytEnabled): ?>
<div class="notice">
<strong>Stream downloading is disabled on this server.</strong><br>
The operator has not enabled video/audio downloads (platform terms require explicit opt-in).
Metadata tools like the <a href="/youtube-thumbnail-downloader">thumbnail downloader</a> and
<a href="/youtube-metadata-viewer">metadata viewer</a> work normally.
</div>
<?php elseif (! $binariesOk): ?>
<div class="notice">
<strong>This tool is temporarily unavailable on this server.</strong><br>
A required processing component is missing. Please check back later.
</div>
<?php elseif ($kind === 'url'): ?>
<form class="url-form" id="tool-form" method="post" action="/api/jobs">
<label for="url-input" class="sr-only" style="position:absolute;left:-9999px">Media URL</label>
<div class="url-row">
<input class="input-lg" type="url" id="url-input" name="url"
placeholder="Paste YouTube URL… https://www.youtube.com/watch?v=…" required>
<button class="btn-primary" type="submit"><?= str_contains(strtolower($tool['name']), 'download') || str_contains(strtolower($tool['name']), 'mp3') ? 'Convert' : 'Analyze' ?></button>
</div>
<span class="hint">Works with watch links, Shorts and share links.</span>
</form>
<div class="result-panel" id="info-panel"></div>
<div class="status-flow" id="status-flow" hidden>
<div class="progress-track"><div class="progress-fill" id="progress-fill"></div></div>
<ul class="stages">
<li data-stage="preparing">Preparing</li>
<li data-stage="fetching">Fetching information</li>
<li data-stage="processing">Processing</li>
<li data-stage="converting">Converting</li>
<li data-stage="complete">Complete</li>
</ul>
</div>
<div class="result-panel" id="result-panel"></div>
<p class="notice error" id="error-box" hidden role="alert"></p>
<?php elseif ($kind === 'text'): ?>
<form id="tool-form" data-client="<?= esc((string) $tool['operation']) ?>">
<textarea class="tool-area" id="tool-input" placeholder="<?= esc(textPlaceholder((string) $tool['operation'])) ?>"></textarea>
<?php if ($tool['operation'] === 'qr_generate'): ?>
<div class="opt-grid" style="padding-top:.8rem">
<div><label for="qr-size">Size</label>
<select id="qr-size">
<option value="200">Small (200px)</option>
<option value="300" selected>Medium (300px)</option>
<option value="500">Large (500px)</option>
<option value="800">Print (800px)</option>
</select></div>
<div><label for="qr-level">Error correction</label>
<select id="qr-level">
<option value="L">Low — smallest code</option>
<option value="M" selected>Medium — recommended</option>
<option value="Q">Quartile</option>
<option value="H">High — best for print/logos</option>
</select></div>
</div>
<?php endif ?>
<div class="opt-grid" style="padding-top:.8rem">
<button class="btn-primary" type="submit" style="min-width:180px"><?= icon('arrow') ?> Run</button>
</div>
</form>
<div class="result-panel" id="client-result"></div>
<?php else: ?>
<form id="tool-form" enctype="multipart/form-data">
<div class="dropzone" id="dropzone" tabindex="0" role="button" aria-label="Upload a file">
<?= icon('file') ?>
<strong>Drop your file here</strong>
<span>or click to browse —
<?= esc(implode(', ', array_map('strtoupper', array_slice($tool['input_formats'] ?: ['any format'], 0, 8)))) ?>
· up to <?= (int) ($tool['max_upload_mb'] ?: config('Site')->maxUploadMb) ?> MB
</span>
<input type="file" id="file-input" hidden
accept="<?= esc('.' . implode(',.', array_merge($tool['input_formats'] ?: [], ['mp4', 'jpg', 'png', 'webp', 'gif', 'mp3', 'wav', 'pdf', 'm4a', 'flac', 'ogg', 'mov', 'mkv', 'avi', 'srt', 'vtt', 'svg', 'bmp', 'avif']))) ?>">
</div>
<div class="file-chip" id="file-chip" hidden>
<?= icon('file') ?>
<span class="name" id="chip-name"></span>
<span class="size hint" id="chip-size"></span>
<button type="button" id="chip-remove">×</button>
</div>
<?php if (! empty($tool['output_formats'])): ?>
<details class="advanced">
<summary>Advanced options</summary>
<div class="opt-grid">
<?php if (count($tool['output_formats']) > 1 && in_array($tool['operation'], ['convert', 'extract_audio'], true)): ?>
<div><label for="opt-format">Output format</label>
<select id="opt-format">
<?php foreach ($tool['output_formats'] as $fmt): ?>
<option value="<?= esc($fmt) ?>" <?= $fmt === $tool['primary_output_format'] ? 'selected' : '' ?>><?= esc(strtoupper($fmt)) ?></option>
<?php endforeach ?>
</select></div>
<?php endif ?>
<?php if (in_array(\App\Libraries\FormatCatalog::family((string) ($tool['primary_output_format'] ?? '')), ['audio'], true) || in_array($tool['operation'], ['extract_audio'], true)): ?>
<div><label for="opt-bitrate">Audio bitrate</label>
<select id="opt-bitrate"><option value="320">320 kbps</option><option value="192" selected>192 kbps</option><option value="128">128 kbps</option></select></div>
<?php endif ?>
<?php if ($tool['operation'] === 'compress'): ?>
<div><label for="opt-level">Compression strength</label>
<select id="opt-level"><option value="20">Sharper file</option><option value="28" selected>Balanced</option><option value="34">Smaller file</option></select></div>
<?php endif ?>
<?php if (in_array($tool['operation'], ['resize', 'resolution'], true)): ?>
<div><label for="opt-width">Width (px)</label><input id="opt-width" type="number" min="16" max="7680" placeholder="auto"></div>
<div><label for="opt-height">Height (px)</label><input id="opt-height" type="number" min="16" max="4320" placeholder="auto"></div>
<?php endif ?>
<?php if (in_array($tool['operation'], ['trim', 'cut'], true)): ?>
<div><label for="opt-start">Start (seconds)</label><input id="opt-start" type="number" min="0" step="0.1" value="0"></div>
<div><label for="opt-end">End (seconds)</label><input id="opt-end" type="number" min="0" step="0.1" placeholder="end of file"></div>
<?php endif ?>
<?php if ($tool['operation'] === 'rotate'): ?>
<div><label for="opt-degrees">Rotation</label>
<select id="opt-degrees"><option value="90">90° clockwise</option><option value="180">180°</option><option value="270">90° counter-clockwise</option></select></div>
<?php endif ?>
<?php if ($tool['operation'] === 'flip'): ?>
<div><label for="opt-direction">Direction</label>
<select id="opt-direction"><option value="horizontal">Horizontal mirror</option><option value="vertical">Vertical flip</option></select></div>
<?php endif ?>
<?php if ($tool['operation'] === 'speed'): ?>
<div><label for="opt-factor">Speed factor</label>
<input id="opt-factor" type="number" step="0.05" min="0.25" max="4" value="1.5"></div>
<?php endif ?>
<?php if (in_array($tool['operation'], ['fps', 'extract_frames'], true)): ?>
<div><label for="opt-fps">Frames per second</label>
<input id="opt-fps" type="number" min="1" max="60" value="<?= $tool['operation'] === 'fps' ? 30 : 2 ?>"></div>
<?php endif ?>
<?php if ($tool['operation'] === 'volume'): ?>
<div><label for="opt-gain">Gain (dB)</label>
<input id="opt-gain" type="number" min="-30" max="30" value="6"></div>
<?php endif ?>
<?php if ($tool['operation'] === 'pdf_compress'): ?>
<div><label for="opt-preset">Preset</label>
<select id="opt-preset"><option value="ebook" selected>Ebook (150 dpi)</option><option value="screen">Screen (72 dpi)</option><option value="printer">Printer (300 dpi)</option></select></div>
<?php endif ?>
<?php if ($tool['operation'] === 'pdf_extract_pages'): ?>
<div><label for="opt-pages">Pages</label>
<input id="opt-pages" type="text" placeholder="e.g. 1-3 or 2,5,7"></div>
<?php endif ?>
</div>
</details>
<?php endif ?>
<div style="margin-top:1rem">
<button class="btn-primary" type="submit" id="convert-btn" disabled><?= icon('arrow') ?> Convert</button>
</div>
</form>
<div class="status-flow" id="status-flow" hidden>
<div class="progress-track"><div class="progress-fill" id="progress-fill"></div></div>
<ul class="stages">
<li data-stage="preparing">Preparing</li>
<li data-stage="processing">Processing</li>
<li data-stage="converting">Converting</li>
<li data-stage="complete">Complete</li>
</ul>
</div>
<div class="result-panel" id="result-panel"></div>
<p class="notice error" id="error-box" hidden role="alert"></p>
<?php endif ?>
</section>
<?php if (! empty($tool['how_to'])): ?>
<section class="block tight">
<h2 class="section-title">How to use the <?= esc(mb_strtolower((string) $tool['name'])) ?></h2>
<ol class="steps">
<?php foreach ($tool['how_to'] as $step): ?>
<li><strong><?= esc($step['name']) ?></strong><?= esc($step['text']) ?></li>
<?php endforeach ?>
</ol>
</section>
<?php endif ?>
<?php if (! empty($tool['input_formats']) || ! empty($tool['output_formats'])): ?>
<section class="block tight">
<h2 class="section-title">Supported formats</h2>
<?php if (! empty($tool['input_formats'])): ?>
<p style="margin:.2rem 0 .3rem;font-weight:650;font-size:.9rem">Input</p>
<div class="chips"><?php foreach ($tool['input_formats'] as $f): ?><a class="chip" href="/<?= esc($f) ?>-converter"><?= esc(strtoupper($f)) ?></a><?php endforeach ?></div>
<?php endif ?>
<?php if (! empty($tool['output_formats'])): ?>
<p style="margin:.9rem 0 .3rem;font-weight:650;font-size:.9rem">Output</p>
<div class="chips"><?php foreach ($tool['output_formats'] as $f): ?><a class="chip" href="/<?= esc(($tool['primary_input_format'] ?? '') !== '' ? $tool['primary_input_format'] . '-to-' . $f : $f . '-converter') ?>"><?= esc(strtoupper($f)) ?></a><?php endforeach ?></div>
<?php endif ?>
</section>
<?php endif ?>
<?= view('partials/faq', ['faqs' => is_array($tool['faqs']) ? $tool['faqs'] : []]) ?>
<?php if (! empty($guides)): ?>
<section class="block tight">
<h2 class="section-title">Related guides</h2>
<div class="grid cols-3">
<?php foreach ($guides as $guide): ?>
<a class="card" href="/blog/<?= esc($guide['slug']) ?>">
<span class="badge">Guide · <?= (int) $guide['reading_minutes'] ?> min</span>
<h3 style="margin-top:.5rem"><?= esc($guide['title']) ?></h3>
<p><?= esc(mb_substr((string) $guide['excerpt'], 0, 120)) ?>…</p>
</a>
<?php endforeach ?>
</div>
</section>
<?php endif ?>
<?php if (! empty($conversions)): ?>
<?= view('partials/tools_grid', ['tools' => $conversions, 'title' => 'Related conversions']) ?>
<?php endif ?>
<?= view('partials/tools_grid', ['tools' => $related, 'title' => 'Related tools']) ?>
</div>