whisper.cat stuff

This commit is contained in:
[Harper Innes]
2023-10-05 23:28:32 +11:00
parent 2a6e28637c
commit f127b4fea8
5224 changed files with 919361 additions and 0 deletions

View File

@ -0,0 +1,386 @@
'use strict';
// Async base64 encoding and decoding
// Bundles TextEncoderLite and b64.js for utf8 and typed array support.
// See: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
function asyncToBase64(value, encoding, onSuccess, onError) {
try {
switch (encoding) {
case 'ascii':
var result = window.btoa(value);
onSuccess(result);
break;
case 'utf8':
value = Base64Utils.encodeToUtf8Array(value);
// Fall through into byte array case.
case undefined:
var result = Base64Utils.uint8ToBase64(value);
onSuccess(result);
break;
default:
onError('Unknown encoding \'' + encoding + '\'.');
}
} catch (error) {
if (error.name === 'InvalidCharacterError') {
onError('Couldn\'t convert non-latin1 characters to base64.');
} else if (error.message) {
onError('Couldn\'t convert to base64 because ' + error.message + '.');
} else {
onError('Couldn\'t convert to base64 because ' + error + '.');
}
}
}
function asyncFromBase64(value, encoding, onSuccess, onError) {
try {
switch (encoding) {
case 'ascii':
var result = window.atob(Base64Utils.cleanupBase64(value));
onSuccess(result);
break;
case 'utf8':
var utf8ByteArray = Base64Utils.b64ToByteArray(Base64Utils.cleanupBase64(value));
var result = Base64Utils.decodeFromUtf8Array(utf8ByteArray);
onSuccess(result);
break;
default:
onError('Unknown encoding \'' + encoding + '\'.');
}
} catch (error) {
if (error.name == 'InvalidCharacterError') {
onError('Couldn\'t convert because the base64 decodes to non-latin1 characters.');
} else if (error.message) {
onError('Couldn\'t convert from base64 because ' + error.message + '.');
} else {
onError('Couldn\'t convert from base64 because ' + error + '.');
}
}
}
// Utility helper functions:
// cleanupBase64, uint8ToBase64, b64ToByteArray, encodeToUtf8Array, decodeFromUtf8Array
function Base64Utils() {
}
// Remove invalid characters and correct padding to accept more liberal input.
// This is modified from https://github.com/feross/buffer/blob/master/index.js by feross et. al.
Base64Utils.cleanupBase64 = function(dirty) {
// Trim whitespace.
dirty = dirty.trim ? dirty.trim() : dirty.replace(/^\s+|\s+$/g, '');
// Remove anything outside our range.
var INVALID_BASE64_RE = /[^+\/0-9A-Za-z-_]/g;
dirty = dirty.replace(INVALID_BASE64_RE, '');
if (dirty.length < 2)
return '';
// Ensure padding is correct.
while (dirty.length % 4 !== 0)
dirty = dirty + '=';
return dirty;
}
// b64.js from https://github.com/beatgammit/base64-js by beatgammit, feross, and others.
// Modified to only export uint8ToBase64 and b64ToByteArray.
//
// The MIT License (MIT)
//
// Copyright (c) 2014
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
;(function (exports) {
var i, code, lookup, revLookup, Arr;
function initIfNeeded() {
if (code !== undefined)
return;
code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
lookup = [];
for (i = 0; i < code.length; i++)
lookup[i] = code[i];
revLookup = [];
for (i = 0; i < code.length; ++i)
revLookup[code.charCodeAt(i)] = i;
revLookup['-'.charCodeAt(0)] = 62;
revLookup['_'.charCodeAt(0)] = 63;
Arr = (typeof Uint8Array !== 'undefined') ? Uint8Array : Array;
}
function decode(elt) {
var v = revLookup[elt.charCodeAt(0)];
return v !== undefined ? v : -1;
}
function b64ToByteArray(b64) {
initIfNeeded();
var i, j, l, tmp, placeHolders, arr;
if (b64.length % 4 > 0)
throw new Error('the length is not a multiple of 4');
// the number of equal signs (place holders)
// if there are two placeholders, than the two characters before it
// represent one byte
// if there is only one, then the three characters before it represent 2 bytes
// this is just a cheap hack to not do indexOf twice
var len = b64.length;
placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0;
// base64 is 4/3 + up to two characters of the original data
arr = new Arr(b64.length * 3 / 4 - placeHolders);
// if there are placeholders, only get up to the last complete 4 chars
l = placeHolders > 0 ? b64.length - 4 : b64.length;
var L = 0;
function push(v) {
arr[L++] = v;
}
for (i = 0, j = 0; i < l; i += 4, j += 3) {
tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3));
push((tmp & 0xFF0000) >> 16);
push((tmp & 0xFF00) >> 8);
push(tmp & 0xFF);
}
if (placeHolders === 2) {
tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4);
push(tmp & 0xFF);
} else if (placeHolders === 1) {
tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2);
push((tmp >> 8) & 0xFF);
push(tmp & 0xFF);
}
return arr;
}
function encode(num) {
return lookup[num];
}
function tripletToBase64(num) {
return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F);
}
function encodeChunk(uint8, start, end) {
var temp;
var output = [];
for (var i = start; i < end; i += 3) {
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]);
output.push(tripletToBase64(temp));
}
return output.join('');
}
function uint8ToBase64(uint8) {
initIfNeeded();
var i;
var extraBytes = uint8.length % 3; // if we have 1 byte left, pad 2 bytes
var output = '';
var parts = [];
var temp, length;
var maxChunkLength = 16383; // must be multiple of 3
// go through the array every three bytes, we'll deal with trailing stuff later
for (i = 0, length = uint8.length - extraBytes; i < length; i += maxChunkLength)
parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > length ? length : (i + maxChunkLength)));
// pad the end with zeros, but make sure to not forget the extra bytes
switch (extraBytes) {
case 1:
temp = uint8[uint8.length - 1];
output += encode(temp >> 2);
output += encode((temp << 4) & 0x3F);
output += '==';
break;
case 2:
temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1]);
output += encode(temp >> 10);
output += encode((temp >> 4) & 0x3F);
output += encode((temp << 2) & 0x3F);
output += '=';
break;
default:
break;
}
parts.push(output);
return parts.join('');
}
exports.uint8ToBase64 = uint8ToBase64;
exports.b64ToByteArray = b64ToByteArray;
}(Base64Utils));
// TextEncoderLite from https://github.com/coolaj86/TextEncoderLite/blob/master/index.js by
// coolaj86, feross, and others.
// Modified to only export encodeToUtf8Array and decodeFromUtf8Array.
//
// The MIT License (MIT)
//
// Copyright (c) Feross Aboukhadijeh, and other contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
;(function (exports) {
function utf8ToBytes(string, units) {
units = units || Infinity;
var codePoint;
var length = string.length;
var leadSurrogate = null;
var bytes = [];
for (var i = 0; i < length; i++) {
codePoint = string.charCodeAt(i);
// is surrogate component
if (codePoint > 0xD7FF && codePoint < 0xE000) {
// last char was a lead
if (leadSurrogate) {
// 2 leads in a row
if (codePoint < 0xDC00) {
if ((units -= 3) > -1)
bytes.push(0xEF, 0xBF, 0xBD);
leadSurrogate = codePoint;
continue;
} else {
// valid surrogate pair
codePoint = leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00 | 0x10000;
leadSurrogate = null;
}
} else {
// no lead yet
if (codePoint > 0xDBFF) {
// unexpected trail
if ((units -= 3) > -1)
bytes.push(0xEF, 0xBF, 0xBD);
continue;
} else if (i + 1 === length) {
// unpaired lead
if ((units -= 3) > -1)
bytes.push(0xEF, 0xBF, 0xBD);
continue;
} else {
// valid lead
leadSurrogate = codePoint;
continue;
}
}
} else if (leadSurrogate) {
// valid bmp char, but last char was a lead
if ((units -= 3) > -1)
bytes.push(0xEF, 0xBF, 0xBD);
leadSurrogate = null;
}
// encode utf8
if (codePoint < 0x80) {
if ((units -= 1) < 0)
break;
bytes.push(codePoint);
} else if (codePoint < 0x800) {
if ((units -= 2) < 0)
break;
bytes.push(
codePoint >> 0x6 | 0xC0,
codePoint & 0x3F | 0x80
);
} else if (codePoint < 0x10000) {
if ((units -= 3) < 0)
break;
bytes.push(
codePoint >> 0xC | 0xE0,
codePoint >> 0x6 & 0x3F | 0x80,
codePoint & 0x3F | 0x80
);
} else if (codePoint < 0x200000) {
if ((units -= 4) < 0)
break;
bytes.push(
codePoint >> 0x12 | 0xF0,
codePoint >> 0xC & 0x3F | 0x80,
codePoint >> 0x6 & 0x3F | 0x80,
codePoint & 0x3F | 0x80
);
} else {
throw new Error('Invalid code point');
}
}
return bytes;
}
function utf8Slice(buf, start, end) {
var res = '';
var tmp = '';
end = Math.min(buf.length, end || Infinity);
start = start || 0;
for (var i = start; i < end; i++) {
if (buf[i] <= 0x7F) {
res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i]);
tmp = '';
} else {
tmp += '%' + buf[i].toString(16);
}
}
return res + decodeUtf8Char(tmp);
}
function decodeUtf8Char(str) {
try {
return decodeURIComponent(str);
} catch (err) {
return String.fromCharCode(0xFFFD); // UTF 8 invalid char
}
}
exports.encodeToUtf8Array = function(str) {
var result;
if ('undefined' === typeof Uint8Array)
result = utf8ToBytes(str);
else
result = new Uint8Array(utf8ToBytes(str));
return result;
}
exports.decodeFromUtf8Array = function(bytes) {
return utf8Slice(bytes, 0, bytes.length);
}
}(Base64Utils));

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,74 @@
// Build step, just type:
// gulp
//
// Prereqs:
// 1) Install Node and NPM from https://nodejs.org
// Known to work with Node 4.2.6 and NPM 2.14.12
// 2) Install dependencies from package.json
// npm install
//
// The purpose of this build step is just to crush the output filesize. Everything should work
// when loaded directly out of the root directory without running this build step.
var gulp = require('gulp');
var del = require('del');
var imagemin = require('gulp-imagemin');
var pngquant = require('imagemin-pngquant');
var inlineSource = require('gulp-inline-source');
var htmlmin = require('gulp-htmlmin');
var uglify = require('gulp-uglify');
var outDir = 'out/';
var paths = {
html: ['index.html'],
images: ['images/*.png'],
extras: ['manifest.json', 'favicon.ico'],
scripts: ['simple-offline-service-worker.js']
};
gulp.task('clean', function() {
return del(outDir);
});
// Optimize and copy images
gulp.task('images', ['clean'], function() {
var imageminOptions = {
use: [pngquant({quality: '65-80', speed: 1})]
};
return gulp.src(paths.images)
.pipe(imagemin(imageminOptions))
.pipe(gulp.dest(outDir + 'images/'));
});
gulp.task('minifyHtmlCssJs', ['clean'], function() {
var inlineSourceOptions = {
compress: false
};
var htmlminOptions = {
collapseWhitespace: true,
minifyJS: true,
minifyCSS: true,
preserveLineBreaks: true,
removeComments: true
};
return gulp.src(paths.html)
.pipe(inlineSource(inlineSourceOptions))
.pipe(htmlmin(htmlminOptions))
.pipe(gulp.dest(outDir));
});
gulp.task('minifyExternalScripts', ['clean'], function() {
return gulp.src(paths.scripts)
.pipe(uglify())
.pipe(gulp.dest(outDir));
});
gulp.task('copy', ['clean'], function() {
// Copy extras
gulp.src(paths.extras)
.pipe(gulp.dest(outDir));
});
gulp.task('default', ['clean', 'copy', 'images', 'minifyHtmlCssJs', 'minifyExternalScripts']);

View File

@ -0,0 +1,322 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, minimum-scale=1.0, user-scalable=no">
<meta name="description" content="A fast and simple tool to convert to and from base64.">
<meta name="theme-color" content="#2196f3">
<link rel="manifest" href="manifest.json">
<link rel="icon" href="favicon.ico">
<meta http-equiv="cleartype" content="on">
<title>WebOasis - Base64 Converter</title>
<style>
html, body {
height: 100%;
margin: 0;
}
body {
background: #fafafa;
font-family: 'Roboto', 'Helvetica', 'Trebuchet MS1', sans-serif;
-webkit-font-smoothing: antialiased;
/* Disable rubber-band effect on scroll. */
-ms-touch-action: none;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
}
.header {
background: #0078d4;
box-shadow: 0 2px 5px rgba(0,0,0,0.26);
color: #fff;
font-size: 20px;
font-weight: 400;
padding: 0.65em;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.topRight {
color: rgba(250,250,250,0.8);
float: right;
font-size: 14px;
/* This aligns the link when directly next to the header. */
padding-top: 4px;
}
.topRight:hover, .topRight:focus {
color: rgb(250,250,250);
}
.io {
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
min-height: 0;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: row;
flex-direction: row;
}
.tile {
background: #fff;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
margin: 2em;
/* When squished (mobile landscape), have a minimum standard of readability. */
min-height: 7em;
display: -webkit-flex;
display: flex;
-webkit-flex: 1 0 auto;
flex: 1 0 auto;
-webkit-flex-direction: column;
flex-direction: column;
width: 0;
}
/* Ensure there's not twice the padding when side-by-side. */
#leftTile { margin-right: 1em; }
#rightTile { margin-left: 1em; }
.type {
border-bottom: 1px solid #dbdbdb;
color: rgba(0,0,0,0.87);
font-size: 18px;
font-weight: 500;
padding: 0.65em;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.content {
font-family: monospace;
font-size: 14px;
height: 100%;
margin: 0;
padding: 0;
position: relative;
/* Margins must equal the total padding on children to ensure scrollbars stay flush. */
margin-bottom: 2em;
margin-right: 2em;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
}
.textarea {
border: none;
font-family: inherit;
font-size: inherit;
height: 100%;
margin: 0;
/* Workaround to improve experience on browsers without new flexbox. */
min-height: 3em;
outline: none;
overflow: auto;
/* This padding must match the imagearea's padding. */
padding: 1em;
position: absolute;
resize: none;
width: 100%;
}
.textarea[readonly], .textarea[readonly='readonly'] {
background: #fff;
color: rgba(0,0,0,0.54);
}
.hidden {
display: none;
}
.errorSource {
background-color: #ffebee;
color: rgba(0,0,0,0.38);
}
.errorDestination {
color: rgba(0,0,0,0.38);
}
.imagearea {
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
font-family: inherit;
font-size: inherit;
height: 100%;
/* Workaround to improve experience on browsers without new flexbox. */
min-height: 3em;
margin: 0;
overflow: auto;
/* This padding must match the textarea's padding. */
padding: 1em;
position: absolute;
width: 100%;
}
/* Custom upload button to workaround multiple cross-browser issues. */
/* The basic approach is to hide an upload control on top of a button image. */
.customUpload {
bottom: 0;
height: 60px;
left: 0;
margin: auto;
overflow: hidden;
position: absolute;
right: 0;
top: 0;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
width: 60px;
}
/* When an image is present, move button to the top-left. */
.imagearea.containsImage .customUpload {
margin: 0;
position: relative;
}
.customUpload svg {
/* drop-shadow does not support the exact blur syntax so this has been approximated. */
-webkit-filter: drop-shadow(0 2px 1px rgba(0,0,0,0.14)) drop-shadow(0 1px 5px rgba(0,0,0,0.12)) drop-shadow(0 1px 1px rgba(0,0,0,0.1));
filter: drop-shadow(0 2px 1px rgba(0,0,0,0.14)) drop-shadow(0 1px 5px rgba(0,0,0,0.12)) drop-shadow(0 1px 1px rgba(0,0,0,0.1));
height: 100%;
width: 100%;
}
.customUpload svg path {
fill: #2196f3;
}
.customUpload:hover svg path {
fill: #1976d2;
}
.customUpload input {
cursor: pointer;
height: 200%;
left: 0;
opacity: 0;
position: absolute;
/* Disable blue glow on Chrome for Android due to cursor: pointer. */
-webkit-tap-highlight-color: transparent;
top: -100%;
width: 100%;
/* A filter on the svg will give it a stacking context, so ensure this stays on top. */
z-index: 99;
}
/* Drag and drop styles. */
#leftTile .dragover {
background-color: #e1f5fe;
}
/* On small screens, stack the areas without margin. */
@media (max-width: 1000px) {
.io {
-webkit-flex-direction: column;
flex-direction: column;
}
.tile {
border-radius: 0;
box-shadow: none;
margin: 0;
-webkit-flex: 1 0 auto;
flex: 1 0 auto;
height: 0;
width: 100%;
}
/* Remove side-by-side padding special-case. */
#leftTile { margin-right: 0; }
#rightTile { margin-left: 0; }
/* Workaround for crbug.com/586872 and Android 4.3 where the background is visible. */
body {
background: #fff;
}
}
/* Custom select box to workaround multiple cross-browser issues. */
/* The basic idea is to stack an invisible select above custom text and a dropdown icon. */
.customSelect {
border-bottom: 1px solid #dbdbdb;
display: inline-block;
margin: 0;
padding: 0;
position: relative;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
}
.customSelect svg {
display: inline-block;
height: 1em;
vertical-align: text-bottom;
width: 1em;
/* Hairline 2px adjustment just for aesthetics. */
margin-left: -2px;
}
.customSelect svg path {
fill: rgba(0,0,0,0.87);
}
.customSelect select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
box-shadow: none;
font: inherit;
/* Position at +/- 10% to increase hit-testable area. */
height: 120%;
left: -10%;
margin: 0;
opacity: 0;
padding: 0;
position: absolute;
top: -10%;
width: 120%;
}
.customSelect select:focus {
outline: none;
}
/* Custom focus support because we cannot use parent CSS selectors. */
.customSelect#focused {
border-bottom: 1px solid rgba(0,0,0,0.87);
}
</style>
</head>
<body>
<div class="header">WebOasis - Base64 Converter<a class="topRight" href="../">Go Back</a></div>
<div class="io">
<div id="leftTile" class="tile">
<div class="type">
<div class="customSelect">
<select id="leftTypeSelect">
<option value="text" selected="true">Text</option>
<option value="image">Image</option>
</select>
<span id="leftTypeText">Text</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-100 -100 300 300">
<path d="M0 0L100 0L50 80Z" />
</svg>
</div>
</div>
<div id="leftContent" class="content">
<textarea id="leftTextarea" class="textarea" spellcheck="false">Hello world</textarea>
<div id="leftImageArea" class="imagearea hidden">
<div class="customUpload">
<input id="leftImageInput" type="file" accept="image/*" title="">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" />
</svg>
</div>
</div>
</div>
</div>
<div id="rightTile" class="tile">
<div class="type">
<div class="customSelect">
<select id="rightTypeSelect">
<option value="base64utf8" selected="true">Base64 (utf8)</option>
<option value="base64ascii">Base64 (ascii)</option>
<option value="base64image" disabled="true">Base64 (image)</option>
</select>
<span id="rightTypeText">Base64</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-100 -100 300 300">
<path d="M0 0L100 0L50 80Z" />
</svg>
</div>
</div>
<div class="content">
<textarea id="rightTextarea" class="textarea" spellcheck="false">SGVsbG8gd29ybGQ=</textarea>
</div>
</div>
</div>
</body>
<script src="base64.js" inline></script>
<script src="index.js" inline></script>
</html>

View File

@ -0,0 +1,289 @@
'use strict';
// Business logic for base64 encoder (index.html).
// Requires asyncToBase64 and asyncFromBase64 from base64.js
// Initialization of these conversion type constants and locals is done in DOMContentLoaded.
var LeftConversionTypes, RightConversionTypes;
// These state variables are for the selected conversion types, initialized in DOMContentLoaded.
var leftConversionType, rightConversionType;
// This state variable lets us change the conversion type without overwriting the user's last input.
var userLastChangedRightSide = false;
// FIXME: Check if this can be cleared more often to reduce peak memory usage.
var currentFile;
window.addEventListener('DOMContentLoaded', function() {
// Must keep in sync with #leftTypeSelect's DOM.
LeftConversionTypes = {
'text': {displayName: 'Text', isImage: false},
'image': {displayName: 'Image', isImage: true}
}
// Must keep in sync with the #rightTypeSelect's DOM.
RightConversionTypes = {
'base64utf8': {displayName: 'Base64', encoding: 'utf8', forImage: false},
'base64ascii': {displayName: 'Base64 (ascii)', encoding: 'ascii', forImage: false},
'base64image': {displayName: 'Base64 (image)', encoding: 'utf8', forImage: true}
}
leftConversionType = LeftConversionTypes[Object.keys(LeftConversionTypes)[0]];
rightConversionType = RightConversionTypes[Object.keys(RightConversionTypes)[0]];
leftTextarea.addEventListener('input', function() {
userLastChangedRightSide = false;
updateConversion();
});
leftImageInput.addEventListener('change', function(event) {
if (event.target.files[0]) {
currentFile = event.target.files[0];
userLastChangedRightSide = false;
updateConversion();
}
});
rightTextarea.addEventListener('input', function() {
userLastChangedRightSide = true;
updateConversion();
});
leftTypeSelect.addEventListener('change', function() {
onLeftTypeChanged();
});
rightTypeSelect.addEventListener('change', function() {
onRightTypeChanged();
});
// Ensure there is a focus effect on the custom type select boxes.
leftTypeSelect.addEventListener('focus', function(event) {
leftTypeSelect.parentElement.setAttribute('id', 'focused');
});
leftTypeSelect.addEventListener('blur', function(event) {
leftTypeSelect.parentElement.removeAttribute('id');
});
rightTypeSelect.addEventListener('focus', function(event) {
rightTypeSelect.parentElement.setAttribute('id', 'focused');
});
rightTypeSelect.addEventListener('blur', function(event) {
rightTypeSelect.parentElement.removeAttribute('id');
});
// Drag and drop support.
var insideCount = 0;
leftContent.addEventListener('dragenter', function(event) {
insideCount++;
leftTextarea.classList.add('dragover');
leftImageArea.classList.add('dragover');
});
leftContent.addEventListener('dragleave', function(event) {
if (--insideCount === 0) {
leftTextarea.classList.remove('dragover');
leftImageArea.classList.remove('dragover');
}
});
document.body.addEventListener('dragover', function(event) {
// Required for the drop event to work properly.
event.preventDefault();
});
document.body.addEventListener('drop', function(event) {
event.preventDefault();
event.stopPropagation();
insideCount = 0;
leftTextarea.classList.remove('dragover');
leftImageArea.classList.remove('dragover');
if (event.dataTransfer.files.length > 0)
onDropFiles(event.dataTransfer.files);
});
// Update state if a select value is already present (e.g., if the user navigated back.)
var leftConversionTypeFromDOM = LeftConversionTypes[leftTypeSelect.value];
if (leftConversionTypeFromDOM !== leftConversionType)
onLeftTypeChanged();
var rightConversionTypeFromDOM = RightConversionTypes[rightTypeSelect.value];
if (rightConversionTypeFromDOM !== rightConversionType)
onRightTypeChanged();
// Bootup our service worker for offline support.
if ('serviceWorker' in navigator)
navigator.serviceWorker.register('simple-offline-service-worker.js');
});
function updateConversion() {
// Cleanup any leftover errors.
leftImageArea.classList.remove('errorSource');
leftTextarea.classList.remove('errorSource');
rightTextarea.classList.remove('errorSource');
leftTextarea.classList.remove('errorDestination');
rightTextarea.classList.remove('errorDestination');
if (leftConversionType.isImage)
convertImage();
else
bidirectionalTextConversion(!userLastChangedRightSide);
}
// TODO: Gracefully disable this if the FileReader/etc APIs are not available.
function convertImage() {
var setBackgroundImage = function(optionalDataUri) {
if (optionalDataUri) {
leftImageArea.classList.add('containsImage');
leftImageArea.style.backgroundImage = 'url(' + optionalDataUri + ')';
} else {
leftImageArea.classList.remove('containsImage');
leftImageArea.style.backgroundImage = '';
}
}
var success = function(value) {
var dataUri = 'data:' + imageType(currentFile) + ';base64,' + value;
rightTextarea.value = '<img src="' + dataUri + '">';
setBackgroundImage(dataUri);
}
var error = function(message) {
leftImageArea.classList.add('errorSource');
rightTextarea.classList.add('errorDestination');
rightTextarea.value = message;
setBackgroundImage(undefined);
}
if (!currentFile) {
rightTextarea.value = 'No image';
setBackgroundImage(undefined);
return;
}
if (imageType(currentFile).indexOf('image') === -1) {
error('Image type \'' + imageType(currentFile) + '\' was not recognized.');
return;
}
var reader = new FileReader();
reader.onload = function(result) {
var imageFileAsArray = new Uint8Array(result.target.result);
asyncToBase64(imageFileAsArray, undefined, success, error);
};
reader.onerror = function(result) {
error(result.message);
};
rightTextarea.value = 'Converting...';
reader.readAsArrayBuffer(currentFile);
}
function bidirectionalTextConversion(convertLeftToRight) {
var inputElement = convertLeftToRight ? leftTextarea : rightTextarea;
var outputElement = convertLeftToRight ? rightTextarea : leftTextarea;
var success = function(value) {
outputElement.value = value;
}
var error = function(message) {
inputElement.classList.add('errorSource');
outputElement.classList.add('errorDestination');
outputElement.value = message;
}
var converter = convertLeftToRight ? asyncToBase64 : asyncFromBase64;
converter(inputElement.value, rightConversionType.encoding, success, error);
}
function onLeftTypeChanged(optionalSkipConversionUpdate) {
var selectedValue = leftTypeSelect.options[leftTypeSelect.selectedIndex].value;
var newType = LeftConversionTypes[selectedValue];
if (newType === leftConversionType)
return;
if (newType === undefined)
throw 'Unknown conversion type: ' + selectedValue;
leftConversionType = newType;
leftTypeText.textContent = leftConversionType.displayName;
// Ensure the correct (image / text) left view is visible.
if (leftConversionType.isImage == leftImageArea.classList.contains('hidden')) {
leftImageArea.classList.toggle('hidden');
leftTextarea.classList.toggle('hidden');
}
updateRightTypeAfterLeftTypeChange();
if (!optionalSkipConversionUpdate)
updateConversion();
}
function onRightTypeChanged(optionalSkipConversionUpdate) {
var selectedValue = rightTypeSelect.options[rightTypeSelect.selectedIndex].value;
var newType = RightConversionTypes[selectedValue];
if (newType === rightConversionType)
return;
if (newType === undefined)
throw 'Unknown conversion type: ' + selectedValue;
rightConversionType = newType;
rightTypeText.textContent = rightConversionType.displayName;
if (rightConversionType.forImage)
rightTextarea.setAttribute('readonly', 'readonly');
else
rightTextarea.removeAttribute('readonly');
if (rightConversionType.forImage)
userLastChangedRightSide = false;
if (!optionalSkipConversionUpdate)
updateConversion();
}
function updateRightTypeAfterLeftTypeChange() {
var firstEnabledIndex, firstEnabledType;
for (var optionIndex = 0; optionIndex < rightTypeSelect.options.length; optionIndex++) {
var option = rightTypeSelect.options[optionIndex];
var rightType = RightConversionTypes[option.value];
if (rightType === undefined)
throw 'Unknown conversion type: ' + selectedValue;
var enabled = leftConversionType.isImage === rightType.forImage;
option.disabled = !enabled;
if (!firstEnabledIndex && !firstEnabledType && enabled) {
firstEnabledIndex = optionIndex;
firstEnabledType = rightType;
}
}
if (leftConversionType.isImage != rightConversionType.forImage) {
rightTypeSelect.selectedIndex = firstEnabledIndex;
onRightTypeChanged(true);
}
}
function onDropFiles(files) {
if (files.length === 0)
return;
currentFile = files[0];
if (!leftConversionType.isImage) {
// Switch to the first image type.
var leftConversions = Object.keys(LeftConversionTypes);
for (var index = 0; index < leftConversions.length; index++) {
if (LeftConversionTypes[leftConversions[index]].isImage) {
leftTypeSelect.selectedIndex = index;
onLeftTypeChanged(true);
break;
}
}
}
updateConversion();
}
function imageType(file) {
if (file.type !== '')
return file.type;
// The stock browser on Android 4.3 returns an empty string for type. If the type is empty but a
// file was uploaded, synthesize a mimetype from the filename.
if (!file.name || file.name.length <= 0)
return '';
var extension = file.name.substr(file.name.lastIndexOf('.') + 1);
switch (extension) {
case('bmp'): return 'image/bmp';
case('gif'): return 'image/gif';
case('jpeg'): return 'image/jpeg';
case('jpg'): return 'image/jpeg';
case('jpe'): return 'image/jpeg';
case('png'): return 'image/png';
case('svg'): return 'image/svg+xml';
case('svgz'): return 'image/svg+xml';
case('webp'): return 'image/webp';
case('ico'): return 'image/x-icon';
}
return '';
}

View File

@ -0,0 +1,21 @@
{
"short_name": "Base64",
"name": "Base64 Converter",
"icons": [
{
"src": "images/appicon192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/appicon144.png",
"sizes": "144x144",
"type": "image/png"
}
],
"start_url": "./",
"display": "standalone",
"orientation": "any",
"background_color": "#fafafa",
"theme_color": "#2196f3"
}

View File

@ -0,0 +1,19 @@
{
"private": true,
"name": "Base64",
"version": "1.0.0",
"description": "Base64 Converter",
"main": "index.html",
"devDependencies": {
"del": "^2.2.0",
"gulp": "^3.9.1",
"gulp-htmlmin": "^1.3.0",
"gulp-imagemin": "^2.4.0",
"gulp-inline-source": "^2.1.0",
"gulp-uglify": "^1.5.3",
"imagemin-pngquant": "^4.2.0"
},
"engines": {
"node": ">=0.10.0"
}
}

View File

@ -0,0 +1,65 @@
'use strict';
// Simple offline service worker.
//
// On the main page include:
// if ('serviceWorker' in navigator)
// navigator.serviceWorker.register('simple-offline-service-worker.js');
//
// This cache always requests from the network to keep the cache fresh. There is a tradeoff here
// because it'll wait ages for a slow network despite having a cached response ready to go. If stale
// content is acceptable, use an 'eventually fresh' approach as described by Jake Archibald or
// Nicolás Bevacqua in https://ponyfoo.com/articles/progressive-networking-serviceworker
var version = 'v4.2.0';
// To cache https://yoururl/subdirectory/ without listing index.html, use './'. If using a manifest,
// 'start_url' should also be './'. You can also add additional files to this list.
var offlineFiles = [ './' ];
self.addEventListener('install', function(event) {
// Cache our list of files.
event.waitUntil(
caches.open(version).then(function(cache) {
return cache.addAll(offlineFiles).then(function() {
return self.skipWaiting();
});
})
);
});
// Remove any stale cache entries.
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(keys) {
return Promise.all(keys.map(function(key) {
if (key !== version)
return caches.delete(key);
}));
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
// FIXME: We should send off the fetch request and check the cache in parallel.
fetch(event.request).then(function(networkReponse) {
// Check if this request is already in our cache. We only want to cache previously
// cached items to prevent the cache from getting polluted.
return caches.open(version).then(function(cache) {
return cache.match(event.request).then(function(cachedResponse) {
if (!cachedResponse)
return networkReponse;
// Clone the response since we're also returning it below.
cache.put(event.request, networkReponse.clone());
return networkReponse;
});
});
}).catch(function(networkError) {
return caches.open(version).then(function(cache) {
return cache.match(event.request).then(function(cachedResponse) {
return cachedResponse || networkError;
});
});
})
);
});