PairDrop/public/scripts/util.js

600 lines
20 KiB
JavaScript

// Polyfill for Navigator.clipboard.writeText
if (!navigator.clipboard) {
navigator.clipboard = {
writeText: text => {
// A <span> contains the text to copy
const span = document.createElement('span');
span.innerText = text;
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
// Paint the span outside the viewport
span.style.position = 'absolute';
span.style.left = '-9999px';
span.style.top = '-9999px';
const win = window;
const selection = win.getSelection();
win.document.body.appendChild(span);
const range = win.document.createRange();
selection.removeAllRanges();
range.selectNode(span);
selection.addRange(range);
let success = false;
try {
success = win.document.execCommand('copy');
} catch (err) {
return Promise.error();
}
selection.removeAllRanges();
span.remove();
return Promise.resolve();
}
}
}
// Polyfills
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
window.hiddenProperty = 'hidden' in document
? 'hidden'
: 'webkitHidden' in document
? 'webkitHidden'
: 'mozHidden' in document
? 'mozHidden'
: null;
window.visibilityChangeEvent = 'visibilitychange' in document
? 'visibilitychange'
: 'webkitvisibilitychange' in document
? 'webkitvisibilitychange'
: 'mozvisibilitychange' in document
? 'mozvisibilitychange'
: null;
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent);
window.isMobile = window.iOS || window.android;
// Helper functions
const zipper = (() => {
let zipWriter;
return {
createNewZipWriter() {
zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), { bufferedWrite: true, level: 0 });
},
addFile(file, options) {
return zipWriter.add(file.name, new zip.BlobReader(file), options);
},
async getBlobURL() {
if (zipWriter) {
const blobURL = URL.createObjectURL(await zipWriter.close());
zipWriter = null;
return blobURL;
}
else {
throw new Error("Zip file closed");
}
},
async getZipFile(filename = "archive.zip") {
if (zipWriter) {
const file = new File([await zipWriter.close()], filename, {type: "application/zip"});
zipWriter = null;
return file;
}
else {
throw new Error("Zip file closed");
}
},
async getEntries(file, options) {
return await (new zip.ZipReader(new zip.BlobReader(file))).getEntries(options);
},
async getData(entry, options) {
return await entry.getData(new zip.BlobWriter(), options);
},
};
})();
const mime = (() => {
const suffixToMimeMap = {
"cpl": "application/cpl+xml",
"gpx": "application/gpx+xml",
"gz": "application/gzip",
"jar": "application/java-archive",
"war": "application/java-archive",
"ear": "application/java-archive",
"class": "application/java-vm",
"js": "application/javascript",
"mjs": "application/javascript",
"json": "application/json",
"map": "application/json",
"webmanifest": "application/manifest+json",
"doc": "application/msword",
"dot": "application/msword",
"wiz": "application/msword",
"bin": "application/octet-stream",
"dms": "application/octet-stream",
"lrf": "application/octet-stream",
"mar": "application/octet-stream",
"so": "application/octet-stream",
"dist": "application/octet-stream",
"distz": "application/octet-stream",
"pkg": "application/octet-stream",
"bpk": "application/octet-stream",
"dump": "application/octet-stream",
"elc": "application/octet-stream",
"deploy": "application/octet-stream",
"img": "application/octet-stream",
"msp": "application/octet-stream",
"msm": "application/octet-stream",
"buffer": "application/octet-stream",
"oda": "application/oda",
"oxps": "application/oxps",
"pdf": "application/pdf",
"asc": "application/pgp-signature",
"sig": "application/pgp-signature",
"prf": "application/pics-rules",
"p7c": "application/pkcs7-mime",
"cer": "application/pkix-cert",
"ai": "application/postscript",
"eps": "application/postscript",
"ps": "application/postscript",
"apk": "application/vnd.android.package-archive",
"m3u8": "application/vnd.apple.mpegurl",
"pkpass": "application/vnd.apple.pkpass",
"kml": "application/vnd.google-earth.kml+xml",
"kmz": "application/vnd.google-earth.kmz",
"cab": "application/vnd.ms-cab-compressed",
"xls": "application/vnd.ms-excel",
"xlm": "application/vnd.ms-excel",
"xla": "application/vnd.ms-excel",
"xlc": "application/vnd.ms-excel",
"xlt": "application/vnd.ms-excel",
"xlw": "application/vnd.ms-excel",
"msg": "application/vnd.ms-outlook",
"ppt": "application/vnd.ms-powerpoint",
"pot": "application/vnd.ms-powerpoint",
"ppa": "application/vnd.ms-powerpoint",
"pps": "application/vnd.ms-powerpoint",
"pwz": "application/vnd.ms-powerpoint",
"mpp": "application/vnd.ms-project",
"mpt": "application/vnd.ms-project",
"xps": "application/vnd.ms-xpsdocument",
"odb": "application/vnd.oasis.opendocument.database",
"ods": "application/vnd.oasis.opendocument.spreadsheet",
"odt": "application/vnd.oasis.opendocument.text",
"osm": "application/vnd.openstreetmap.data+xml",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"pcap": "application/vnd.tcpdump.pcap",
"cap": "application/vnd.tcpdump.pcap",
"dmp": "application/vnd.tcpdump.pcap",
"wpd": "application/vnd.wordperfect",
"wasm": "application/wasm",
"7z": "application/x-7z-compressed",
"dmg": "application/x-apple-diskimage",
"bcpio": "application/x-bcpio",
"torrent": "application/x-bittorrent",
"cbr": "application/x-cbr",
"cba": "application/x-cbr",
"cbt": "application/x-cbr",
"cbz": "application/x-cbr",
"cb7": "application/x-cbr",
"vcd": "application/x-cdlink",
"crx": "application/x-chrome-extension",
"cpio": "application/x-cpio",
"csh": "application/x-csh",
"deb": "application/x-debian-package",
"udeb": "application/x-debian-package",
"dvi": "application/x-dvi",
"arc": "application/x-freearc",
"gtar": "application/x-gtar",
"hdf": "application/x-hdf",
"h5": "application/x-hdf5",
"php": "application/x-httpd-php",
"iso": "application/x-iso9660-image",
"key": "application/x-iwork-keynote-sffkey",
"numbers": "application/x-iwork-numbers-sffnumbers",
"pages": "application/x-iwork-pages-sffpages",
"latex": "application/x-latex",
"run": "application/x-makeself",
"mif": "application/x-mif",
"lnk": "application/x-ms-shortcut",
"mdb": "application/x-msaccess",
"exe": "application/x-msdownload",
"dll": "application/x-msdownload",
"com": "application/x-msdownload",
"bat": "application/x-msdownload",
"msi": "application/x-msdownload",
"pub": "application/x-mspublisher",
"cdf": "application/x-netcdf",
"nc": "application/x-netcdf",
"pl": "application/x-perl",
"pm": "application/x-perl",
"prc": "application/x-pilot",
"pdb": "application/x-pilot",
"p12": "application/x-pkcs12",
"pfx": "application/x-pkcs12",
"ram": "application/x-pn-realaudio",
"pyc": "application/x-python-code",
"pyo": "application/x-python-code",
"rar": "application/x-rar-compressed",
"rpm": "application/x-redhat-package-manager",
"sh": "application/x-sh",
"shar": "application/x-shar",
"swf": "application/x-shockwave-flash",
"sql": "application/x-sql",
"srt": "application/x-subrip",
"sv4cpio": "application/x-sv4cpio",
"sv4crc": "application/x-sv4crc",
"gam": "application/x-tads",
"tar": "application/x-tar",
"tcl": "application/x-tcl",
"tex": "application/x-tex",
"roff": "application/x-troff",
"t": "application/x-troff",
"tr": "application/x-troff",
"man": "application/x-troff-man",
"me": "application/x-troff-me",
"ms": "application/x-troff-ms",
"ustar": "application/x-ustar",
"src": "application/x-wais-source",
"xpi": "application/x-xpinstall",
"xhtml": "application/xhtml+xml",
"xht": "application/xhtml+xml",
"xsl": "application/xml",
"rdf": "application/xml",
"wsdl": "application/xml",
"xpdl": "application/xml",
"zip": "application/zip",
"3gp": "audio/3gp",
"3gpp": "audio/3gpp",
"3g2": "audio/3gpp2",
"3gpp2": "audio/3gpp2",
"aac": "audio/aac",
"adts": "audio/aac",
"loas": "audio/aac",
"ass": "audio/aac",
"au": "audio/basic",
"snd": "audio/basic",
"mid": "audio/midi",
"midi": "audio/midi",
"kar": "audio/midi",
"rmi": "audio/midi",
"mpga": "audio/mpeg",
"mp2": "audio/mpeg",
"mp2a": "audio/mpeg",
"mp3": "audio/mpeg",
"m2a": "audio/mpeg",
"m3a": "audio/mpeg",
"oga": "audio/ogg",
"ogg": "audio/ogg",
"spx": "audio/ogg",
"opus": "audio/opus",
"aif": "audio/x-aiff",
"aifc": "audio/x-aiff",
"aiff": "audio/x-aiff",
"flac": "audio/x-flac",
"m4a": "audio/x-m4a",
"m3u": "audio/x-mpegurl",
"wma": "audio/x-ms-wma",
"ra": "audio/x-pn-realaudio",
"wav": "audio/x-wav",
"otf": "font/otf",
"ttf": "font/ttf",
"woff": "font/woff",
"woff2": "font/woff2",
"emf": "image/emf",
"gif": "image/gif",
"heic": "image/heic",
"heif": "image/heif",
"ief": "image/ief",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"pict": "image/pict",
"pct": "image/pict",
"pic": "image/pict",
"png": "image/png",
"svg": "image/svg+xml",
"svgz": "image/svg+xml",
"tif": "image/tiff",
"tiff": "image/tiff",
"psd": "image/vnd.adobe.photoshop",
"djvu": "image/vnd.djvu",
"djv": "image/vnd.djvu",
"dwg": "image/vnd.dwg",
"dxf": "image/vnd.dxf",
"dds": "image/vnd.ms-dds",
"webp": "image/webp",
"3ds": "image/x-3ds",
"ras": "image/x-cmu-raster",
"ico": "image/x-icon",
"bmp": "image/x-ms-bmp",
"pnm": "image/x-portable-anymap",
"pbm": "image/x-portable-bitmap",
"pgm": "image/x-portable-graymap",
"ppm": "image/x-portable-pixmap",
"rgb": "image/x-rgb",
"tga": "image/x-tga",
"xbm": "image/x-xbitmap",
"xpm": "image/x-xpixmap",
"xwd": "image/x-xwindowdump",
"eml": "message/rfc822",
"mht": "message/rfc822",
"mhtml": "message/rfc822",
"nws": "message/rfc822",
"obj": "model/obj",
"stl": "model/stl",
"dae": "model/vnd.collada+xml",
"ics": "text/calendar",
"ifb": "text/calendar",
"css": "text/css",
"csv": "text/csv",
"html": "text/html",
"htm": "text/html",
"shtml": "text/html",
"markdown": "text/markdown",
"md": "text/markdown",
"txt": "text/plain",
"text": "text/plain",
"conf": "text/plain",
"def": "text/plain",
"list": "text/plain",
"log": "text/plain",
"in": "text/plain",
"ini": "text/plain",
"rtx": "text/richtext",
"rtf": "text/rtf",
"tsv": "text/tab-separated-values",
"c": "text/x-c",
"cc": "text/x-c",
"cxx": "text/x-c",
"cpp": "text/x-c",
"h": "text/x-c",
"hh": "text/x-c",
"dic": "text/x-c",
"java": "text/x-java-source",
"lua": "text/x-lua",
"py": "text/x-python",
"etx": "text/x-setext",
"sgm": "text/x-sgml",
"sgml": "text/x-sgml",
"vcf": "text/x-vcard",
"xml": "text/xml",
"xul": "text/xul",
"yaml": "text/yaml",
"yml": "text/yaml",
"ts": "video/mp2t",
"mp4": "video/mp4",
"mp4v": "video/mp4",
"mpg4": "video/mp4",
"mpeg": "video/mpeg",
"m1v": "video/mpeg",
"mpa": "video/mpeg",
"mpe": "video/mpeg",
"mpg": "video/mpeg",
"mov": "video/quicktime",
"qt": "video/quicktime",
"webm": "video/webm",
"flv": "video/x-flv",
"m4v": "video/x-m4v",
"asf": "video/x-ms-asf",
"asx": "video/x-ms-asf",
"vob": "video/x-ms-vob",
"wmv": "video/x-ms-wmv",
"avi": "video/x-msvideo",
"*": "video/x-sgi-movie",
"kdbx": "application/x-keepass2"
}
return {
guessMimeByFilename(filename) {
const split = filename.split('.');
if (split.length === 1) {
// Filename does not include suffix
return false;
}
const suffix = split[split.length - 1].toLowerCase();
return suffixToMimeMap[suffix];
},
addMissingMimeTypesToFiles(files) {
// if filetype is empty guess via suffix otherwise leave unchanged
for (let i = 0; i < files.length; i++) {
if (!files[i].type) {
files[i] = new File([files[i]], files[i].name, {type: mime.guessMimeByFilename(files[i].name) || "application/octet-stream"});
}
}
return files;
}
};
})();
/*
cyrb53 (c) 2018 bryc (github.com/bryc)
A fast and simple hash function with decent collision resistance.
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
Public domain. Attribution appreciated.
*/
const cyrb53 = function(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}
function getUrlWithoutArguments() {
return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
}
function changeFavicon(src) {
document.querySelector('[rel="icon"]').href = src;
document.querySelector('[rel="shortcut icon"]').href = src;
}
function arrayBufferToBase64(buffer) {
let binary = '';
let bytes = new Uint8Array(buffer);
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa( binary );
}
function base64ToArrayBuffer(base64) {
let binary_string = window.atob(base64);
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
async function fileToBlob (file) {
return new Blob([new Uint8Array(await file.arrayBuffer())], {type: file.type});
}
function getThumbnailAsDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
return new Promise(async (resolve, reject) => {
try {
if (file.type === "image/heif" || file.type === "image/heic") {
// hotfix: Converting heic images taken on iOS 18 crashes page. Waiting for PR #350
reject(new Error(`Hotfix: Converting of HEIC/HEIF images currently disabled.`));
return;
// // browsers can't show heic files --> convert to jpeg before creating thumbnail
// let blob = await fileToBlob(file);
// file = await heic2any({
// blob,
// toType: "image/jpeg",
// quality: quality
// });
}
let imageUrl = URL.createObjectURL(file);
let image = new Image();
image.src = imageUrl;
await waitUntilImageIsLoaded(imageUrl);
let imageWidth = image.width;
let imageHeight = image.height;
let canvas = document.createElement('canvas');
// resize the canvas and draw the image data into it
if (width && height) {
canvas.width = width;
canvas.height = height;
}
else if (width) {
canvas.width = width;
canvas.height = Math.floor(imageHeight * width / imageWidth)
}
else if (height) {
canvas.width = Math.floor(imageWidth * height / imageHeight);
canvas.height = height;
}
else {
canvas.width = imageWidth;
canvas.height = imageHeight
}
let ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
let dataUrl = canvas.toDataURL("image/jpeg", quality);
resolve(dataUrl);
} catch (e) {
console.error(e);
reject(new Error(`Could not create an image thumbnail from type ${file.type}`));
}
})
}
// Resolves returned promise when image is loaded and throws error if image cannot be shown
function waitUntilImageIsLoaded(imageUrl, timeout = 10000) {
return new Promise((resolve, reject) => {
let image = new Image();
image.src = imageUrl;
const onLoad = () => {
cleanup();
resolve();
};
const onError = () => {
cleanup();
reject(new Error('Image failed to load.'));
};
const cleanup = () => {
clearTimeout(timeoutId);
image.onload = null;
image.onerror = null;
URL.revokeObjectURL(imageUrl);
};
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error('Image loading timed out.'));
}, timeout);
image.onload = onLoad;
image.onerror = onError;
});
}
async function decodeBase64Files(base64) {
if (!base64) throw new Error('Base64 is empty');
let bstr = atob(base64), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const zipBlob = new File([u8arr], 'archive.zip');
let files = [];
const zipEntries = await zipper.getEntries(zipBlob);
for (let i = 0; i < zipEntries.length; i++) {
let fileBlob = await zipper.getData(zipEntries[i]);
files.push(new File([fileBlob], zipEntries[i].filename));
}
return files
}
async function decodeBase64Text(base64) {
if (!base64) throw new Error('Base64 is empty');
return decodeURIComponent(escape(window.atob(base64)))
}
function isUrlValid(url) {
try {
new URL(url);
return true;
}
catch (e) {
return false;
}
}