3ee7d7a3ac
Single-file browser tool that decodes a base64 string and saves the result back out as a file. No server, no dependencies, nothing uploaded. Input cleanup covers data URIs, URL-safe alphabet, missing padding, line wrapping, percent-encoding and wrapping quotes. Type detection runs magic bytes (~30 signatures), then the data URI's declared MIME, then UTF-8 content sniffing for SVG/HTML/XML/JSON/PEM; unrecognised payloads fall back to .bin with a hex dump. Images, audio, video, PDF and text are previewed inline before download.
573 lines
21 KiB
HTML
573 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Base64 → File</title>
|
||
<style>
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||
html,body{height:100%}
|
||
body{
|
||
font-family:"Segoe UI",Calibri,Arial,sans-serif;
|
||
background:#0f1316;color:#c8d8ea;display:flex;flex-direction:column;overflow:hidden
|
||
}
|
||
|
||
/* ── Header ── */
|
||
.header{
|
||
flex-shrink:0;height:54px;display:flex;align-items:center;gap:14px;
|
||
padding:0 22px;
|
||
background:linear-gradient(135deg,#14191b 0%,#0e1e28 60%,#0b2020 100%);
|
||
border-bottom:2px solid transparent;
|
||
border-image:linear-gradient(to right,#3eafff,#2acc94) 1;
|
||
}
|
||
.brand-name{font-size:18px;font-weight:800;letter-spacing:.3px;color:#fff}
|
||
.brand-name em{
|
||
font-style:normal;
|
||
background:linear-gradient(90deg,#3eafff,#2acc94);
|
||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text
|
||
}
|
||
.brand-pipe{color:#2a3a3a;font-size:13px}
|
||
.brand-sub{font-size:12px;color:#4a6878}
|
||
.header-right{margin-left:auto;display:flex;align-items:center;gap:10px}
|
||
.stat-chip{
|
||
background:rgba(62,175,255,.08);border:1px solid rgba(62,175,255,.18);
|
||
border-radius:20px;padding:3px 12px;font-size:11px;color:#5a8898;white-space:nowrap
|
||
}
|
||
.stat-chip strong{color:#2acc94}
|
||
|
||
/* ── Layout ── */
|
||
.layout{display:flex;flex:1;min-height:0}
|
||
.pane{display:flex;flex-direction:column;min-width:0;min-height:0}
|
||
.pane-in{flex:1 1 50%;border-right:1px solid #1a2428}
|
||
.pane-out{flex:1 1 50%;background:#0c1014}
|
||
.pane-head{
|
||
flex-shrink:0;display:flex;align-items:center;gap:10px;
|
||
padding:9px 16px;border-bottom:1px solid #1a2428;background:#0c1014
|
||
}
|
||
.pane-out .pane-head{background:#0a0e11}
|
||
.pane-title{
|
||
font-size:9px;font-weight:700;letter-spacing:2px;
|
||
text-transform:uppercase;color:#2a4048
|
||
}
|
||
.pane-tools{margin-left:auto;display:flex;align-items:center;gap:6px}
|
||
|
||
/* ── Input ── */
|
||
.drop{position:relative;flex:1;display:flex;min-height:0}
|
||
textarea{
|
||
flex:1;resize:none;border:0;outline:0;padding:14px 16px;
|
||
background:#0f1316;color:#a8c4d8;
|
||
font-family:"Cascadia Mono",Consolas,"Courier New",monospace;
|
||
font-size:12px;line-height:1.55;word-break:break-all
|
||
}
|
||
textarea::placeholder{color:#2f4450}
|
||
.drop.over::after{
|
||
content:"Drop file to load its contents";
|
||
position:absolute;inset:8px;border:2px dashed #3eafff;border-radius:8px;
|
||
background:rgba(62,175,255,.07);display:flex;align-items:center;justify-content:center;
|
||
font-size:13px;color:#3eafff;pointer-events:none
|
||
}
|
||
|
||
/* ── Buttons ── */
|
||
button{
|
||
font-family:inherit;font-size:11px;cursor:pointer;
|
||
background:#141e24;color:#6a8898;border:1px solid #1e2c34;
|
||
border-radius:5px;padding:5px 12px;transition:background .1s,color .1s,border-color .1s
|
||
}
|
||
button:hover:not(:disabled){background:#18242c;color:#a0c0d0;border-color:#2a3c46}
|
||
button:disabled{opacity:.35;cursor:not-allowed}
|
||
button.primary{
|
||
background:linear-gradient(90deg,#3eafff,#2acc94);color:#08191f;
|
||
border:0;font-weight:700;padding:7px 18px
|
||
}
|
||
button.primary:hover:not(:disabled){filter:brightness(1.12)}
|
||
|
||
/* ── Output ── */
|
||
.out-body{flex:1;overflow-y:auto;min-height:0;padding:16px}
|
||
.empty{
|
||
height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||
gap:8px;color:#2f4450;font-size:13px;text-align:center;padding:20px
|
||
}
|
||
.empty .big{font-size:30px;opacity:.5}
|
||
|
||
.card{
|
||
background:#0f151a;border:1px solid #1a2428;border-radius:8px;
|
||
padding:14px 16px;margin-bottom:14px
|
||
}
|
||
.card.err{border-color:#5a2430;background:#1a1013}
|
||
.card.err .card-label{color:#e06a7a}
|
||
.card-label{
|
||
font-size:9px;font-weight:700;letter-spacing:2px;text-transform:uppercase;
|
||
color:#2a4048;margin-bottom:10px
|
||
}
|
||
.err-msg{font-size:13px;color:#e8a0aa;line-height:1.5}
|
||
.err-hint{font-size:11px;color:#8a5a64;margin-top:8px;line-height:1.5}
|
||
|
||
.meta{display:grid;grid-template-columns:auto 1fr;gap:7px 16px;font-size:12px}
|
||
.meta dt{color:#4a6878;white-space:nowrap}
|
||
.meta dd{color:#c8d8ea;word-break:break-all}
|
||
.meta dd code{
|
||
font-family:"Cascadia Mono",Consolas,monospace;font-size:11px;
|
||
color:#2acc94;background:#0a1418;border-radius:3px;padding:1px 5px
|
||
}
|
||
|
||
.save-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||
input[type=text]{
|
||
flex:1;min-width:150px;font-family:"Cascadia Mono",Consolas,monospace;font-size:12px;
|
||
background:#0a0e11;color:#c8d8ea;border:1px solid #1e2c34;border-radius:5px;
|
||
padding:7px 10px;outline:0
|
||
}
|
||
input[type=text]:focus{border-color:#3eafff}
|
||
|
||
.preview img,.preview video{
|
||
max-width:100%;max-height:340px;display:block;margin:0 auto;
|
||
border-radius:5px;background:#0a0e11
|
||
}
|
||
.preview audio{width:100%}
|
||
.preview iframe{width:100%;height:420px;border:0;border-radius:5px;background:#fff}
|
||
.preview pre{
|
||
max-height:340px;overflow:auto;
|
||
font-family:"Cascadia Mono",Consolas,monospace;font-size:11.5px;line-height:1.55;
|
||
color:#a8c4d8;white-space:pre-wrap;word-break:break-word
|
||
}
|
||
.hex{
|
||
font-family:"Cascadia Mono",Consolas,monospace;font-size:11.5px;line-height:1.6;
|
||
color:#5a8898;white-space:pre;overflow-x:auto
|
||
}
|
||
.hex b{color:#2acc94;font-weight:400}
|
||
.hex i{color:#2a4048;font-style:normal}
|
||
|
||
@media (max-width:820px){
|
||
body{overflow:auto}
|
||
.layout{flex-direction:column}
|
||
.pane-in{border-right:0;border-bottom:1px solid #1a2428}
|
||
.pane-in .drop{min-height:200px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<span class="brand-name">Base<em>64</em> → File</span>
|
||
<span class="brand-pipe">|</span>
|
||
<span class="brand-sub">decode base64 and save it back out as a file</span>
|
||
<div class="header-right">
|
||
<span class="stat-chip">In <strong id="statIn">0</strong></span>
|
||
<span class="stat-chip">Out <strong id="statOut">0 B</strong></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="layout">
|
||
|
||
<div class="pane pane-in">
|
||
<div class="pane-head">
|
||
<span class="pane-title">Base64 input</span>
|
||
<div class="pane-tools">
|
||
<button id="btnPaste">Paste</button>
|
||
<button id="btnLoad">Load file</button>
|
||
<button id="btnClear">Clear</button>
|
||
</div>
|
||
</div>
|
||
<div class="drop" id="drop">
|
||
<textarea id="input" spellcheck="false" autocomplete="off"
|
||
placeholder="Paste base64 here. Data URIs (data:image/png;base64,...), URL-safe base64, missing padding and wrapped lines are all handled automatically."></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pane pane-out">
|
||
<div class="pane-head">
|
||
<span class="pane-title">Decoded file</span>
|
||
<div class="pane-tools">
|
||
<button id="btnCopy" disabled>Copy text</button>
|
||
<button class="primary" id="btnSave" disabled>Download</button>
|
||
</div>
|
||
</div>
|
||
<div class="out-body" id="out">
|
||
<div class="empty">
|
||
<div class="big">⇩</div>
|
||
<div>Paste or drop some base64 to get started</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<input type="file" id="filePicker" hidden>
|
||
|
||
<script>
|
||
(function(){
|
||
"use strict";
|
||
|
||
var input = document.getElementById('input');
|
||
var out = document.getElementById('out');
|
||
var drop = document.getElementById('drop');
|
||
var filePicker = document.getElementById('filePicker');
|
||
var statIn = document.getElementById('statIn');
|
||
var statOut = document.getElementById('statOut');
|
||
var btnSave = document.getElementById('btnSave');
|
||
var btnCopy = document.getElementById('btnCopy');
|
||
|
||
var state = { bytes:null, name:'', mime:'', text:null, url:null };
|
||
|
||
/* ── Magic-byte signatures ──────────────────────────────────────────
|
||
Checked in order; first match wins. `at` is the byte offset of the
|
||
signature, `sig` is a byte array (null = wildcard for that byte). */
|
||
var SIGS = [
|
||
{ ext:'png', mime:'image/png', at:0, sig:[0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A] },
|
||
{ ext:'jpg', mime:'image/jpeg', at:0, sig:[0xFF,0xD8,0xFF] },
|
||
{ ext:'gif', mime:'image/gif', at:0, sig:[0x47,0x49,0x46,0x38] },
|
||
{ ext:'webp', mime:'image/webp', at:8, sig:[0x57,0x45,0x42,0x50] },
|
||
{ ext:'bmp', mime:'image/bmp', at:0, sig:[0x42,0x4D] },
|
||
{ ext:'ico', mime:'image/x-icon', at:0, sig:[0x00,0x00,0x01,0x00] },
|
||
{ ext:'pdf', mime:'application/pdf', at:0, sig:[0x25,0x50,0x44,0x46] },
|
||
{ ext:'wav', mime:'audio/wav', at:8, sig:[0x57,0x41,0x56,0x45] },
|
||
{ ext:'avi', mime:'video/x-msvideo', at:8, sig:[0x41,0x56,0x49,0x20] },
|
||
{ ext:'mp4', mime:'video/mp4', at:4, sig:[0x66,0x74,0x79,0x70] },
|
||
{ ext:'mkv', mime:'video/x-matroska', at:0, sig:[0x1A,0x45,0xDF,0xA3] },
|
||
{ ext:'ogg', mime:'audio/ogg', at:0, sig:[0x4F,0x67,0x67,0x53] },
|
||
{ ext:'flac', mime:'audio/flac', at:0, sig:[0x66,0x4C,0x61,0x43] },
|
||
{ ext:'mp3', mime:'audio/mpeg', at:0, sig:[0x49,0x44,0x33] },
|
||
{ ext:'mp3', mime:'audio/mpeg', at:0, sig:[0xFF,0xFB] },
|
||
{ ext:'zip', mime:'application/zip', at:0, sig:[0x50,0x4B,0x03,0x04] },
|
||
{ ext:'gz', mime:'application/gzip', at:0, sig:[0x1F,0x8B] },
|
||
{ ext:'bz2', mime:'application/x-bzip2', at:0, sig:[0x42,0x5A,0x68] },
|
||
{ ext:'xz', mime:'application/x-xz', at:0, sig:[0xFD,0x37,0x7A,0x58,0x5A,0x00] },
|
||
{ ext:'7z', mime:'application/x-7z-compressed', at:0, sig:[0x37,0x7A,0xBC,0xAF,0x27,0x1C] },
|
||
{ ext:'rar', mime:'application/vnd.rar', at:0, sig:[0x52,0x61,0x72,0x21,0x1A,0x07] },
|
||
{ ext:'tar', mime:'application/x-tar', at:257, sig:[0x75,0x73,0x74,0x61,0x72] },
|
||
{ ext:'sqlite', mime:'application/vnd.sqlite3', at:0,
|
||
sig:[0x53,0x51,0x4C,0x69,0x74,0x65,0x20,0x66,0x6F,0x72,0x6D,0x61,0x74,0x20,0x33,0x00] },
|
||
{ ext:'ttf', mime:'font/ttf', at:0, sig:[0x00,0x01,0x00,0x00,0x00] },
|
||
{ ext:'otf', mime:'font/otf', at:0, sig:[0x4F,0x54,0x54,0x4F] },
|
||
{ ext:'woff', mime:'font/woff', at:0, sig:[0x77,0x4F,0x46,0x46] },
|
||
{ ext:'woff2',mime:'font/woff2', at:0, sig:[0x77,0x4F,0x46,0x32] },
|
||
{ ext:'exe', mime:'application/vnd.microsoft.portable-executable', at:0, sig:[0x4D,0x5A] },
|
||
{ ext:'elf', mime:'application/x-elf', at:0, sig:[0x7F,0x45,0x4C,0x46] },
|
||
{ ext:'class',mime:'application/java-vm', at:0, sig:[0xCA,0xFE,0xBA,0xBE] }
|
||
];
|
||
|
||
function matches(bytes, s){
|
||
if (bytes.length < s.at + s.sig.length) return false;
|
||
for (var i = 0; i < s.sig.length; i++){
|
||
if (s.sig[i] !== null && bytes[s.at + i] !== s.sig[i]) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function sniff(bytes){
|
||
for (var i = 0; i < SIGS.length; i++){
|
||
if (matches(bytes, SIGS[i])) return { ext:SIGS[i].ext, mime:SIGS[i].mime, sure:true };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/* Text-based formats have no magic bytes, so classify by content once
|
||
we know the payload decodes as valid UTF-8. */
|
||
function sniffText(txt){
|
||
var t = txt.replace(/^/, '').trimStart();
|
||
var head = t.slice(0, 400).toLowerCase();
|
||
if (/^<svg[\s>]/.test(head) || (head.indexOf('<?xml') === 0 && head.indexOf('<svg') > -1))
|
||
return { ext:'svg', mime:'image/svg+xml' };
|
||
if (head.indexOf('<!doctype html') === 0 || head.indexOf('<html') === 0)
|
||
return { ext:'html', mime:'text/html' };
|
||
if (head.indexOf('<?xml') === 0)
|
||
return { ext:'xml', mime:'application/xml' };
|
||
if (/^[{\[]/.test(t)){
|
||
try { JSON.parse(t); return { ext:'json', mime:'application/json' }; } catch(e){}
|
||
}
|
||
if (head.indexOf('-----begin ') === 0)
|
||
return { ext:'pem', mime:'application/x-pem-file' };
|
||
if (head.indexOf('#!') === 0)
|
||
return { ext:'sh', mime:'text/x-shellscript' };
|
||
if (/^%pdf/.test(head))
|
||
return { ext:'pdf', mime:'application/pdf' };
|
||
return { ext:'txt', mime:'text/plain' };
|
||
}
|
||
|
||
/* Last resort for a data URI whose payload has no magic bytes — the
|
||
declared MIME type usually names the extension we want. */
|
||
var MIME_EXT = {
|
||
'plain':'txt', 'jpeg':'jpg', 'javascript':'js', 'markdown':'md',
|
||
'octet-stream':'bin', 'msword':'doc', 'mpeg':'mp3', 'quicktime':'mov'
|
||
};
|
||
function extFromMime(mime){
|
||
var sub = (mime.split('/')[1] || '').split(';')[0].split('+')[0].trim().toLowerCase();
|
||
sub = sub.replace(/^(x-|vnd\.)/, '');
|
||
if (MIME_EXT[sub]) return MIME_EXT[sub];
|
||
return /^[a-z0-9]{1,8}$/.test(sub) ? sub : null;
|
||
}
|
||
|
||
function asUtf8(bytes){
|
||
try { return new TextDecoder('utf-8', {fatal:true}).decode(bytes); }
|
||
catch(e){ return null; }
|
||
}
|
||
|
||
function humanSize(n){
|
||
if (n < 1024) return n + ' B';
|
||
var u = ['KB','MB','GB'], i = -1, v = n;
|
||
do { v /= 1024; i++; } while (v >= 1024 && i < u.length - 1);
|
||
return v.toFixed(v < 10 ? 1 : 0) + ' ' + u[i];
|
||
}
|
||
|
||
function esc(s){
|
||
return String(s).replace(/[&<>"]/g, function(c){
|
||
return {'&':'&','<':'<','>':'>','"':'"'}[c];
|
||
});
|
||
}
|
||
|
||
/* ── Decode ──────────────────────────────────────────────────────── */
|
||
|
||
// Strips a data URI prefix, whitespace and quotes; converts URL-safe
|
||
// base64 to standard and restores missing '=' padding.
|
||
function normalise(raw){
|
||
var mime = '';
|
||
var s = raw.trim().replace(/^["'`]|["'`]$/g, '');
|
||
|
||
var m = /^data:([^;,]*)(;[^,]*)?,/i.exec(s);
|
||
if (m){
|
||
if (!/;base64/i.test(m[2] || '')) throw new Error('That data URI is not base64-encoded.');
|
||
mime = m[1] || '';
|
||
s = s.slice(m[0].length);
|
||
}
|
||
|
||
s = s.replace(/\s+/g, '');
|
||
if (/%[0-9a-f]{2}/i.test(s)){
|
||
try { s = decodeURIComponent(s); } catch(e){}
|
||
}
|
||
s = s.replace(/-/g, '+').replace(/_/g, '/');
|
||
s = s.replace(/=+$/, '');
|
||
|
||
var bad = s.match(/[^A-Za-z0-9+/]/);
|
||
if (bad) throw new Error('Not valid base64 — unexpected character "' + bad[0] + '".');
|
||
if (s.length % 4 === 1) throw new Error('Not valid base64 — the input length is truncated.');
|
||
|
||
while (s.length % 4) s += '=';
|
||
return { data:s, mime:mime };
|
||
}
|
||
|
||
function toBytes(b64){
|
||
var bin = atob(b64);
|
||
var bytes = new Uint8Array(bin.length);
|
||
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||
return bytes;
|
||
}
|
||
|
||
function hexDump(bytes, rows){
|
||
var lines = [], limit = Math.min(bytes.length, rows * 16);
|
||
for (var o = 0; o < limit; o += 16){
|
||
var hex = '', txt = '';
|
||
for (var i = 0; i < 16; i++){
|
||
if (o + i < limit){
|
||
var b = bytes[o + i];
|
||
hex += (b < 16 ? '0' : '') + b.toString(16) + ' ';
|
||
txt += (b >= 32 && b < 127) ? esc(String.fromCharCode(b)) : '.';
|
||
} else {
|
||
hex += ' '; txt += ' ';
|
||
}
|
||
if (i === 7) hex += ' ';
|
||
}
|
||
lines.push('<i>' + ('0000000' + o.toString(16)).slice(-8) + '</i> ' + hex + ' <b>' + txt + '</b>');
|
||
}
|
||
if (bytes.length > limit) lines.push('<i>' + humanSize(bytes.length - limit) + ' more…</i>');
|
||
return lines.join('\n');
|
||
}
|
||
|
||
function reset(){
|
||
if (state.url) URL.revokeObjectURL(state.url);
|
||
state = { bytes:null, name:'', mime:'', text:null, url:null };
|
||
btnSave.disabled = true;
|
||
btnCopy.disabled = true;
|
||
statOut.textContent = '0 B';
|
||
}
|
||
|
||
function showEmpty(){
|
||
reset();
|
||
out.innerHTML = '<div class="empty"><div class="big">⇩</div>' +
|
||
'<div>Paste or drop some base64 to get started</div></div>';
|
||
}
|
||
|
||
function showError(msg, hint){
|
||
reset();
|
||
out.innerHTML = '<div class="card err"><div class="card-label">Could not decode</div>' +
|
||
'<div class="err-msg">' + esc(msg) + '</div>' +
|
||
(hint ? '<div class="err-hint">' + esc(hint) + '</div>' : '') + '</div>';
|
||
}
|
||
|
||
function decode(){
|
||
var raw = input.value;
|
||
statIn.textContent = raw.replace(/\s+/g, '').length.toLocaleString();
|
||
|
||
if (!raw.trim()){ showEmpty(); return; }
|
||
|
||
var norm, bytes;
|
||
try {
|
||
norm = normalise(raw);
|
||
bytes = toBytes(norm.data);
|
||
} catch(e){
|
||
showError(e.message, 'Check that the whole string was copied and that nothing ' +
|
||
'was truncated or line-wrapped with stray characters.');
|
||
return;
|
||
}
|
||
|
||
if (!bytes.length){ showError('The input decoded to zero bytes.'); return; }
|
||
|
||
var text = asUtf8(bytes);
|
||
var hit = sniff(bytes);
|
||
var kind;
|
||
if (hit) kind = hit;
|
||
else if (text !== null) kind = sniffText(text);
|
||
else kind = { ext:'bin', mime:'application/octet-stream' };
|
||
|
||
var mime = norm.mime || kind.mime;
|
||
if (!hit && norm.mime) kind.ext = extFromMime(norm.mime) || kind.ext;
|
||
|
||
reset();
|
||
state.bytes = bytes;
|
||
state.mime = mime;
|
||
state.text = (kind.ext === 'txt' || /^text\/|json|xml|svg|javascript|pem|shellscript/.test(kind.mime)) ? text : null;
|
||
state.name = 'decoded.' + kind.ext;
|
||
state.url = URL.createObjectURL(new Blob([bytes], {type:mime}));
|
||
|
||
statOut.textContent = humanSize(bytes.length);
|
||
btnSave.disabled = false;
|
||
btnCopy.disabled = state.text === null;
|
||
|
||
render(bytes, mime, kind, hit, norm.mime);
|
||
}
|
||
|
||
function render(bytes, mime, kind, hit, uriMime){
|
||
var detected = hit ? 'magic bytes'
|
||
: uriMime ? 'data URI header'
|
||
: kind.ext === 'bin' ? 'unrecognised — treated as binary'
|
||
: 'content sniffing';
|
||
|
||
var html =
|
||
'<div class="card"><div class="card-label">Save as</div>' +
|
||
'<div class="save-row">' +
|
||
'<input type="text" id="nameField" value="' + esc(state.name) + '" spellcheck="false">' +
|
||
'<button class="primary" id="btnSave2">Download</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
|
||
'<div class="card"><div class="card-label">File</div>' +
|
||
'<dl class="meta">' +
|
||
'<dt>Type</dt><dd><code>' + esc(mime) + '</code></dd>' +
|
||
'<dt>Size</dt><dd>' + humanSize(bytes.length) +
|
||
' <span style="color:#4a6878">(' + bytes.length.toLocaleString() + ' bytes)</span></dd>' +
|
||
'<dt>Detected via</dt><dd style="color:#4a6878">' + detected + '</dd>' +
|
||
'</dl>' +
|
||
'</div>';
|
||
|
||
var p = '';
|
||
if (/^image\//.test(mime)){
|
||
p = '<img src="' + state.url + '" alt="decoded image">';
|
||
} else if (/^video\//.test(mime)){
|
||
p = '<video src="' + state.url + '" controls></video>';
|
||
} else if (/^audio\//.test(mime)){
|
||
p = '<audio src="' + state.url + '" controls></audio>';
|
||
} else if (mime === 'application/pdf'){
|
||
p = '<iframe src="' + state.url + '"></iframe>';
|
||
} else if (state.text !== null){
|
||
p = '<pre>' + esc(state.text.slice(0, 20000)) +
|
||
(state.text.length > 20000 ? '\n…' : '') + '</pre>';
|
||
} else {
|
||
p = '<div class="hex">' + hexDump(bytes, 12) + '</div>';
|
||
}
|
||
html += '<div class="card preview"><div class="card-label">Preview</div>' + p + '</div>';
|
||
|
||
out.innerHTML = html;
|
||
|
||
var nameField = document.getElementById('nameField');
|
||
nameField.addEventListener('input', function(){ state.name = nameField.value; });
|
||
nameField.addEventListener('keydown', function(e){ if (e.key === 'Enter') save(); });
|
||
document.getElementById('btnSave2').addEventListener('click', save);
|
||
}
|
||
|
||
function save(){
|
||
if (!state.bytes) return;
|
||
var a = document.createElement('a');
|
||
a.href = state.url;
|
||
a.download = (state.name || 'decoded.bin').trim() || 'decoded.bin';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
}
|
||
|
||
/* ── Events ──────────────────────────────────────────────────────── */
|
||
|
||
var timer;
|
||
input.addEventListener('input', function(){
|
||
clearTimeout(timer);
|
||
timer = setTimeout(decode, 120);
|
||
});
|
||
|
||
btnSave.addEventListener('click', save);
|
||
|
||
btnCopy.addEventListener('click', function(){
|
||
if (state.text === null) return;
|
||
navigator.clipboard.writeText(state.text).then(function(){
|
||
btnCopy.textContent = 'Copied';
|
||
setTimeout(function(){ btnCopy.textContent = 'Copy text'; }, 1200);
|
||
});
|
||
});
|
||
|
||
document.getElementById('btnClear').addEventListener('click', function(){
|
||
input.value = '';
|
||
input.focus();
|
||
decode();
|
||
});
|
||
|
||
document.getElementById('btnPaste').addEventListener('click', function(){
|
||
navigator.clipboard.readText().then(function(t){
|
||
input.value = t;
|
||
decode();
|
||
}).catch(function(){
|
||
input.focus();
|
||
});
|
||
});
|
||
|
||
document.getElementById('btnLoad').addEventListener('click', function(){
|
||
filePicker.click();
|
||
});
|
||
|
||
filePicker.addEventListener('change', function(){
|
||
if (filePicker.files[0]) readAsText(filePicker.files[0]);
|
||
filePicker.value = '';
|
||
});
|
||
|
||
function readAsText(file){
|
||
var r = new FileReader();
|
||
r.onload = function(){ input.value = r.result; decode(); };
|
||
r.readAsText(file);
|
||
}
|
||
|
||
['dragenter','dragover'].forEach(function(ev){
|
||
drop.addEventListener(ev, function(e){
|
||
e.preventDefault();
|
||
drop.classList.add('over');
|
||
});
|
||
});
|
||
['dragleave','drop'].forEach(function(ev){
|
||
drop.addEventListener(ev, function(e){
|
||
e.preventDefault();
|
||
if (ev === 'dragleave' && drop.contains(e.relatedTarget)) return;
|
||
drop.classList.remove('over');
|
||
});
|
||
});
|
||
drop.addEventListener('drop', function(e){
|
||
var f = e.dataTransfer.files[0];
|
||
if (f) readAsText(f);
|
||
});
|
||
|
||
window.addEventListener('keydown', function(e){
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 's' && state.bytes){
|
||
e.preventDefault();
|
||
save();
|
||
}
|
||
});
|
||
|
||
input.focus();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|