// ==UserScript==
// @name Blinkist to PDF
// @description Export Blinkist to PDF
// @match https://www.blinkist.com/*/nc/daily*
// @match https://www.blinkist.com/*/content/daily*
// @match https://www.blinkist.com/*/app/daily*
// @match https://www.blinkist.com/*/books/*
// @match https://www.blinkist.com/books/*
// @run-at document-end
// @version 1.3.15
// @grant none
// @namespace https://greasyfork.org/users/213706
// @license MIT
// ==/UserScript==
/* jshint esversion: 9 */
function main() {
if (String.prototype.endsWith == null) {
String.prototype.endsWith = function (s) {
return s === '' || this.slice(-s.length) === s;
};
}
const DAILY = window.location.pathname.endsWith('/daily');
const SPAN = document.createElement('span');
const NAME = "Blinkist to PDF:";
let USE_LEGACY_READER = false;
var jspdfLoaded = false;
var jszipLoaded = false;
var dialog = false;
var loader = null;
//+--------------------------------------------------------
//|
//| ADD BTN
//|
//+--------------------------------------------------------
/**
* Add btn to top right menu
* To export PDF or audio
*/
function init() {
if (document.getElementById('exportPDF')) {
console.log(NAME, 'export PDF already in DOM');
return;
}
// Create buttons
var buttons = document.createElement('div');
buttons.style.paddingLeft = '5px';
buttons.innerHTML = '<div id="loadingPDF"></div>'
+ '<button id="exportPDF" style="cursor:pointer">Export PDF</button>'
+ ' / <button id="exportAudio" style="cursor:pointer">audio</button>';
// Add buttons to header
var preAttempt = addBtn_preAttempt(buttons);
if (preAttempt) {
addEventListener();
}
if(addBtn(buttons)) {
addEventListener();
// Retry until the header is loaded
} else {
var i = 0;
var interval = setInterval(function(){
if(i++ == 20) {
if(!preAttempt) {
console.log(NAME, 'could not find header');
}
clearInterval(interval);
}
if(addBtn(buttons)) {
clearInterval(interval);
addEventListener();
}
}, 1000);
}
}
/**
* Attempts to insert the given DOMElement to the header
* The header might not be initialized yet, or the user might not be logged in
*
* @param DOMElement btn
* @return Boolean
*/
function addBtn_preAttempt(btn) {
// Try section data-test-id
var section = document.querySelector('[data-test-id="header-navigation"]');
if(section) {
var tryBtn = section.lastElementChild.querySelector('button');
if(tryBtn) {
tryBtn.parentNode.insertBefore(btn, tryBtn);
return true;
}
}
return false;
}
/**
* Attempts to insert the given DOMElement to the header
* Returns whether we were successfull or not (header not yet initialised)
*
* @param DOMElement btn
* @return Boolean
*/
function addBtn(btn) {
// Try on .header-content
var header = document.querySelector('.header-content');
if(!header) {
header = document.querySelector('header');
}
if(header) {
header.appendChild(btn);
return true;
}
// Try on .headerV2__user-menu
header = document.querySelector('.headerV2__user-menu');
if(header) {
header.firstElementChild.appendChild(btn);
return true;
}
// Try search bar
var search = document.querySelector('[data-test-id="desktop-search"]');
if(search) {
search.parentNode.insertBefore(btn, search);
return true;
}
return false;
}
/**
* Add event listener on buttons
*/
function addEventListener() {
loader = document.getElementById('loadingPDF');
document.getElementById('exportPDF').addEventListener('click', exportPDF);
document.getElementById('exportAudio').addEventListener('click', exportAudio);
}
//+--------------------------------------------------------
//|
//| GET BLINK'S DATA
//|
//+--------------------------------------------------------
/**
* Retrieve metadata on the current page
* @return dict - {title, author, desc, url}
*/
function getMeta() {
var container, element;
let more = [];
if(DAILY) {
// Daily: version 2021
if(container = document.querySelector('.daily-book__container')) {
let title = container.querySelector('.daily-book__headline').innerText.trim(),
author = container.querySelector('.daily-book__author').innerText.trim().replace(/^(By|Von) /i, ''),
desc = document.querySelector('.book-tabs__content').innerText.trim(),
url = container.querySelector('.daily-book__cta').getAttribute('href');
USE_LEGACY_READER = true;
return {title, author, desc, more, url};
}
// Daily: version 2022
if(container = document.querySelector('article')) {
let title = (element = container.querySelector('h2')).innerText.trim(),
author = (element = element.nextElementSibling).innerText.trim().replace(/^(By|Von) /i, ''),
desc = (element = element.nextElementSibling).innerText.trim(),
url = container.querySelector('a').getAttribute('href');
return {title, author, desc, more, url};
}
return;
}
if(document.querySelector('.book-preview-info__container')
|| document.querySelector('.book-header__join-blinkist')
) {
alert('You don\'t have access to this content (upgrade to premium)');
return;
}
// v1 (2021)
if(container = document.querySelector('.book__header-container')) {
let title = container.querySelector('.book__header__title').innerText.trim(),
author = container.querySelector('.book__header__author').innerText.trim().replace(/^(By|Von) /i, ''),
desc = document.querySelector('.book-tabs-v0__content').innerText.trim(),
btn = container.querySelector('.js-add-to-library-button'),
url = btn ? btn.getAttribute('href') : null;
USE_LEGACY_READER = true;
return {title, author, desc, more, url};
}
// v2 (2022)
if(container = document.querySelector('.book-header')) {
let title = (element = container.querySelector('h1')).outerText.trim(),
author = (element = element.nextElementSibling).innerText.trim().replace(/^(By|Von) /i, ''),
desc = (element = element.nextElementSibling).innerText.trim(),
btn = container.querySelector('.book-header__controls a'),
url = btn ? btn.getAttribute('href') : null;
return {title, author, desc, more, url};
}
// v3 (2023)
if(container = document.querySelector('.min-h-screen')) {
let title = (element = container.querySelector('h1')).outerText.trim(),
author = (element = element.nextElementSibling).innerText.trim().replace(/^(By|Von) /i, ''),
desc = (element = element.nextElementSibling).innerText.trim(),
btn = container.querySelector('[data-test-id="read-button"]'),
url = btn ? btn.getAttribute('href') : null;
// What's it about + about the author
element = container.querySelector('h4').parentElement;
element.querySelectorAll('h4, p').forEach((element) => {
let text = element.innerText.trim();
// Don't add "Original: Blue Ocean Shift © 2017 Vahlen, München"
if (text.startsWith('Original: ')) {
return;
}
more.push({
tag: element.tagName.toUpperCase(),
text,
});
});
return {title, author, desc, more, url};
}
}
/**
* Retrieve the blink's content
* The callback won't be called if the user doesn't have access to the blink's content
*
* @param string url - url of the blink's content
* @param function callback - handle the export
* @param function[optional] callbackMeta - retrieve the meta given to callback
*/
function getArticle(bookId, callback, callbackMeta) {
fetch(`/api/books/${bookId}/chapters`, {
method: 'GET',
credentials: 'include',
headers: {
"x-requested-with": "XMLHttpRequest"
}
})
.then((response) => response.json())
.then((data) => {
return new Promise((resolve) => {
// Query each chapter
let promises = data.chapters.map((chapter) => {
return fetch(`/api/books/${bookId}/chapters/${chapter.id}`, {
method: 'GET',
credentials: 'include',
headers: {
"x-requested-with": "XMLHttpRequest"
}
});
});
// Parse each response's JSON
Promise.all(promises).then((responses) => {
Promise.all(responses.map((response) => response.json())).then((chapters) => {
// Add chapter data to previous object
for(let chapter of chapters) {
let i = chapter.order_no;
Object.assign(data.chapters[i], chapter);
}
// Continue with the filly populated object
resolve(data);
});
});
});
})
.then((data) => {
callback(data);
});
}
/**
* Retrieve the blink's content
* The callback won't be called if the user doesn't have access to the blink's content
*
* @param string url - url of the blink's content
* @param function callback - handle the export
* @param function[optional] callbackMeta - retrieve the meta given to callback
*/
function legacyGetArticle(url, callback, callbackMeta) {
if(!url) {
callback(document.createElement('div'));
} else {
fetch(url).then(response => {
if(response.redirected && response.url.split('/').slice(-2).join('/') == 'nc/plans') {
alert('You don\'t have access to this content (upgrade to premium)');
} else {
return response.text();
}
})
.then(html => {
if(!html) return;
var parser = new DOMParser(),
_dom = parser.parseFromString(html, 'text/html'),
article = _dom.querySelector('.reader__container__content');
if(article) {
callback(article, callbackMeta? callbackMeta(_dom) : null);
}
});
}
}
//+--------------------------------------------------------
//|
//| EXPORT PDF
//|
//+--------------------------------------------------------
var jspdfCallback = null;
/**
* Load JsPDF
*/
function loadJspdf() {
if(jspdfLoaded) {
return;
}
// Action when loaded
var onScriptLoad = () => {
if(jspdfLoaded) {
return;
}
jspdfLoaded = true;
jspdfCallback && jspdfCallback();
};
// Load script
var script = document.createElement('script');
script.setAttribute("type", "application/javascript");
script.setAttribute("src", 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.debug.js');
script.onreadystatechange = function() {
if (this.readyState == 'loaded' || this.readyState == 'complete') onScriptLoad();
};
script.onload = onScriptLoad;
document.body.appendChild(script);
}
/**
* Get the bookId from the URL
* @param string url - /de/nc/new-reader/halt-finden-in-sich-selbst-de
* /de/nc/reader/der-preis-des-profits-de
* @return string - halt-finden-in-sich-selbst-de
*/
function getBookId(url) {
let parts = url.replace(/\/$/, '').split('/');
return parts[parts.length-1];
}
/**
* Action when clicking on Export PDF
*/
function exportPDF() {
loader.classList.add('visible');
// Load JsPDF
loadJspdf();
// Retrieve data then create PDF
var meta = getMeta();
if(!meta) {
console.error(NAME, "exportPDF got empty meta");
return;
}
const url = meta.url;
// Using new reader
if(!USE_LEGACY_READER) {
let bookId = getBookId(url);
getArticle(bookId, (article) => {
if(!jspdfLoaded) {
jspdfCallback = generatePDF.bind(null, article, meta);
} else {
generatePDF(article, meta);
}
});
} else {
legacyGetArticle(url, (article) => {
if(!jspdfLoaded) {
jspdfCallback = legacyGeneratePDF.bind(null, article, meta);
} else {
legacyGeneratePDF(article, meta);
}
});
}
}
/**
* Create PDF with the retrieved infos
* @param DOMElement article
* @param dict meta - {title, author, desc, url}
*/
function generatePDF(article, meta) {
// PDF settings
var format = "a4",
width = 210, // width of 4A in mm
height = 297; // height of 4A in mm
var lMargin = 15,
rMargin = 15,
tMargin = 30,
bMargin = 30,
lineHeightFactor = 1.6;
var h1Size = 30,
h2Size = 20,
h3Size = 15,
textSize = 12;
/*
// List of formats: https://github.com/parallax/jsPDF/blob/master/src/jspdf.js#L271
var format = "b7",
width = 91, // width of 4A in mm
height = 126; // height of 4A in mm
var lMargin = 5,
rMargin = 5,
tMargin = 10,
bMargin = 10,
lineHeightFactor = 1.6;
var h1Size = 20,
h2Size = 15,
h3Size = 12,
textSize = 10;
*/
// Default export is a4 paper, portrait, using millimeters for units
var pdf = new jsPDF({
orientation: "portrait",
unit: "mm",
format: format, // or size in pt, such as [595.28, 841.89]
});
// Page numbering
var page = 1,
top = tMargin,
maxWidth = width-lMargin-rMargin;
// Add front page
top = tMargin*3;
pdf.setFontSize(h1Size);
pdf.setFontType("bold");
pdf.text(cleanup(meta.title), width/2, top, {maxWidth: maxWidth, align: 'center'});
top += pdf.getTextDimensions('X').h * pdf.splitTextToSize(meta.title, maxWidth).length;
pdf.setFontSize(h2Size);
pdf.setFontType("italic");
pdf.text(cleanup(meta.author), width/2, top, {maxWidth: maxWidth, align: 'center'});
top += h2Size;
pdf.setFontSize(h3Size);
pdf.setFontType("normal");
pdf.text(cleanup(meta.desc), lMargin, top, {maxWidth, lineHeightFactor});
// Add pages
function addFooter() {
pdf.text(''+page, width-8, height-8, {align: 'right'});
}
function addText(text) {
pdf.setFontSize(textSize);
pdf.setFontStyle("normal");
// We're splitting the lines ourselves
// To handle the margins between paragraphs
// And to handle the overflow on another page
var lineHeight = pdf.getTextDimensions('X').h * 1.6,
lines = pdf.splitTextToSize(text, maxWidth);
for(var l=0;l<lines.length;l++) {
if(!lines[l]) {
top += lineHeight / 2;
continue;
}
if(top + bMargin > height) {
addFooter();
page++;
pdf.addPage();
top = tMargin;
}
pdf.text(cleanup(lines[l]), lMargin, top);
top += lineHeight;
}
}
// small jump to try having the entire "about" on the same page (du-bist-genug-de)
top += h3Size;
pdf.setFontSize(10);
// What's it about?
if (meta.more.length) {
for (let i=0; i<meta.more.length; i++) {
let { tag, text } = meta.more[i];
if (tag == "H4") {
if (i) {
top += 6;
}
pdf.setFontSize(h3Size);
pdf.setFontStyle("bold");
pdf.text(cleanup(text), lMargin, top, {maxWidth, lineHeightFactor});
top += pdf.getTextDimensions('X').h * pdf.splitTextToSize(text, maxWidth).length * lineHeightFactor;
top += 3;
} else {
addText(text);
}
}
}
top = tMargin;
// Chapters
for(let i=0; i<article.chapters.length; i++) {
var chapter = article.chapters[i];
if(!chapter.text) continue;
pdf.addPage();
top = tMargin;
var h2 = chapter.action_title.trim(),
text = HTMLToText(chapter.text.trim());
// Page title
if(h2) {
pdf.setFontSize(h2Size);
pdf.setFontStyle("bold");
pdf.text(cleanup(h2), lMargin, top, {maxWidth, lineHeightFactor});
top += pdf.getTextDimensions('X').h * pdf.splitTextToSize(h2, maxWidth).length * lineHeightFactor;
top += 3;
}
// Content
if(text) {
// Quote
if(false) {
pdf.setFontSize(h3Size);
pdf.setFontStyle("normal");
pdf.text(cleanup(text), width/2, height/3, {maxWidth: maxWidth-50, lineHeightFactor, align: 'center'});
// Blink
} else {
addText(text);
}
}
// Page footer
addFooter();
page++;
}
loader.classList.remove('visible');
pdf.save(meta.author + ' - ' + meta.title + '.pdf');
}
/**
* Create PDF with the retrieved infos
* @param DOMElement article
* @param dict meta - {title, author, desc, url}
*/
function legacyGeneratePDF(article, meta) {
// PDF settings
var pdf = new jsPDF(),
width = 210, // width of 4A in mm
height = 297; // height of 4A in mm
var lMargin = 15,
rMargin = 15,
tMargin = 30,
bMargin = 30,
lineHeightFactor = 1.6;
// Page numbering
var page = 1,
top = tMargin,
maxWidth = width-lMargin-rMargin;
// Add front page
top = 90;
pdf.setFontSize(30);
pdf.setFontType("bold");
pdf.text(cleanup(meta.title), width/2, top, {maxWidth: maxWidth, align: 'center'});
top += pdf.getTextDimensions('X').h * pdf.splitTextToSize(meta.title, maxWidth).length;
pdf.setFontSize(20);
pdf.setFontType("italic");
pdf.text(cleanup(meta.author), width/2, top, {maxWidth: maxWidth, align: 'center'});
top += 20;
pdf.setFontSize(15);
pdf.setFontType("normal");
pdf.text(cleanup(meta.desc), lMargin, top, {maxWidth, lineHeightFactor});
// Add pages
function addFooter() {
pdf.text(''+page, width-8, height-8, {align: 'right'});
}
top = tMargin;
pdf.setFontSize(10);
for(var i=0; i<article.children.length; i++) {
var child = article.children[i];
if(!child.classList.contains('chapter')) continue;
pdf.addPage();
top = tMargin;
var h2 = child.children[0].innerText.trim(),
text = HTMLToText(child.children[1].innerHTML.trim());
// Page title
if(h2) {
pdf.setFontSize(20);
pdf.setFontStyle("bold");
pdf.text(cleanup(h2), lMargin, top, {maxWidth, lineHeightFactor});
top += pdf.getTextDimensions('X').h * pdf.splitTextToSize(h2, maxWidth).length * lineHeightFactor;
top += 3;
}
// Content
if(text) {
// Quote
if(child.classList.contains('supplement')) {
pdf.setFontSize(15);
pdf.setFontStyle("normal");
pdf.text(cleanup(text), width/2, height/3, {maxWidth: maxWidth-50, lineHeightFactor, align: 'center'});
// Blink
} else {
pdf.setFontSize(12);
pdf.setFontStyle("normal");
// We're splitting the lines ourselves
// To handle the margins between paragraphs
// And to handle the overflow on another page
var lineHeight = pdf.getTextDimensions('X').h * 1.6,
lines = pdf.splitTextToSize(text, maxWidth);
for(var l=0;l<lines.length;l++) {
if(!lines[l]) {
top += lineHeight / 2;
continue;
}
if(top + bMargin > height) {
addFooter();
page++;
pdf.addPage();
top = tMargin;
}
pdf.text(cleanup(lines[l]), lMargin, top);
top += lineHeight;
}
}
}
// Page footer
addFooter();
page++;
}
loader.classList.remove('visible');
pdf.save(meta.author + ' - ' + meta.title + '.pdf');
}
const CHARS_EQUIVALENT = {
"₂": "2",
"’": "'",
"Ş": "S",
"ğ": "g",
"ş": "s",
"\u2060": '', //word-joiner,
"\u202F": ' ', // narrow No-Break Space
"\u2007": ' ', // figure space
};
/**
* Remove problematic characters
* @param string txt
* @return string
*/
function cleanup(txt) {
return txt.replace(/[^\x9 \xA \xD \xE000-\xFFFD \x20-\xD7FF]/g, function(match){
//console.log(match, match in CHARS_EQUIVALENT);
if(match in CHARS_EQUIVALENT) {
return CHARS_EQUIVALENT[match];
}
return match; // lets bet it's a known special char („ “ – …)
});
}
/**
* Replace HTML entities/tags with plain text
* @param string txt
* @return string
*/
function HTMLToText(txt) {
return txt.replace(/\n|\r/g, '')
.replace(/<\/p><p>/g, "\n\n")
.replace(/<\/p>/g, "\n")
.replace(/<\/li>/g, "\n")
.replace(/<[^>]*>/g, '')
.replace(/&\w+;/g, function(match) {
SPAN.innerHTML = match;
return SPAN.innerText;
});
}
//+--------------------------------------------------------
//|
//| EXPORT AUDIO
//|
//+--------------------------------------------------------
var jszipCallback = null;
/**
* Load JsPDF
*/
function loadJszip() {
if(jszipLoaded) {
return;
}
// Action when loaded
var onScriptLoad = () => {
if(jszipLoaded) {
return;
}
jszipLoaded = true;
jszipCallback && jszipCallback();
};
// Load script
var script = document.createElement('script');
script.setAttribute("type", "application/javascript");
script.setAttribute("src", 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.5.0/jszip.js');
script.onreadystatechange = function() {
if (this.readyState == 'loaded' || this.readyState == 'complete') onScriptLoad();
};
script.onload = onScriptLoad;
document.body.appendChild(script);
}
/**
* Get dialog window
* @return DOMElement
*/
function initDialog() {
if(!dialog) {
dialog = createDialog();
}
dialog.children[0].innerHTML = 'Preparing download';
dialog.children[1].innerHTML = 'Retrieving file list';
dialog.classList.add('visible');
}
/**
* Create dialog window
*/
function createDialog() {
// Add div
var dialog = document.createElement('div');
dialog.setAttribute('id', 'topdfdialog');
dialog.setAttribute('tabindex', '0');
dialog.setAttribute('class', 'infodialog');
dialog.innerHTML = '<div class="infodialog--header"></div><div class="infodialog--about"></div>';
document.body.appendChild(dialog);
// Return it
return document.getElementById('topdfdialog');
}
/**
* Action when clicking on Export audio
*/
function exportAudio() {
// Load JsZip
loadJszip();
// Retrieve data then create zip
var meta = getMeta();
if(!meta) {
console.error(NAME, "exportAudio got empty meta");
return;
}
const url = meta.url;
// Using new reader (/de/nc/new-reader/halt-finden-in-sich-selbst-de)
if(!USE_LEGACY_READER) {
let bookId = getBookId(url);
getArticle(bookId, function(article){
getAudio(article, meta);
});
} else {
legacyGetArticle(url, legacyGetAudio, (_dom) => {
var bookContainer = _dom.querySelector('div[data-book-id]'),
bookid = bookContainer.getAttribute('data-book-id'),
csrftoken = _dom.querySelector('meta[name="csrf-token"]').content;
return {...meta, bookid, csrftoken};
});
}
}
/**
* Get the API url for audio
* @param string bookid
* @param string chapterid
* @param string
*/
function legacyGetAudioApiUrl(bookid, chapterid) {
return 'https://www.blinkist.com/api/books/' + bookid + '/chapters/' + chapterid + '/audio';
}
/**
* Retrieve the list of chapters and audio track associated to it
* Then create zip
*/
function getAudio(article, meta) {
// Show dialog
initDialog();
// Retrieve the audio urls of all chapters
var chapters = [],
audios = [];
for(let i=0; i<article.chapters.length; i++) {
var chapter = article.chapters[i];
chapters.push({"h1": chapter.action_title});
audios.push({"url": chapter.signed_audio_url});
}
// Export zip
if(jszipLoaded) {
generateAudioZip(chapters, audios, meta);
} else {
jszipCallback = generateAudioZip.bind(null, chapters, audios, meta);
}
}
/**
* Retrieve the list of chapters and audio track associated to it
* Then create zip
*/
function legacyGetAudio(article, meta) {
// Retrieve list of chapters (id + title)
var chapters = [];
for(var i=0; i<article.children.length; i++) {
var child = article.children[i];
if(!child.classList.contains('chapter')) continue; // .reader_container_buttons
if(child.classList.contains('supplement')) continue; // quote
var chapterid = child.dataset.chapterid,
h1 = child.children[0].innerText.trim();
chapters.push({chapterid, h1});
}
// Show dialog
initDialog();
// Retrieve the audio urls of all chapters
var responses = [];
for(let i=0; i<chapters.length; i++) {
var chapter = chapters[i],
url = legacyGetAudioApiUrl(meta.bookid, chapter.chapterid);
responses.push(fetch(url, {
credentials: "same-origin",
headers: {
'Referer': 'https://www.blinkist.com' + meta.url,
'x-csrf-token': meta.crsftoken,
'x-requested-with': 'XMLHttpRequest',
},
}).then(response => response.json()));
}
// Export zip
Promise.all(responses).then((audios) => {
if(jszipLoaded) {
generateAudioZip(chapters, audios, meta);
} else {
jszipCallback = generateAudioZip.bind(null, chapters, audios, meta);
}
});
}
/**
* Create Zip with the retrieved infos
* @param list chapters
* @param list audios
* @param dict meta - {title, author, desc, url}
*/
async function generateAudioZip(chapters, audios, meta) {
// Update dialog
dialog.children[1].innerHTML = 'Zipping ' + audios.length + ' file(s)';
// Create zip blob
var zip = new JSZip(),
queue = [];
// Add files
for(let i=0; i<chapters.length; i++) {
let chapter = chapters[i],
audio = audios[i];
let url = 'https://cors-anywhere.99901dev.workers.dev/?q=' + encodeURIComponent(audio.url);
queue.push(fetch(url)
.then(response => response.blob())
.then(function(blob) {
let num = (i+1).toString().padStart(2, '0'),
title = cleanupFilename(chapter.h1);
zip.file(`${num} - ${title}.m4a`, blob, {binary: true});
dialog.children[1].innerHTML = (i+1) + ' file(s) zipped';
}));
}
// Download zip
Promise.all(queue).then(() => {
dialog.children[0].innerHTML = 'Download ready';
zip.generateAsync({type:"blob"}).then((blob) => {
var filename = cleanupFilename(meta.author + ' - ' + meta.title) + '.zip';
download(blob, filename);
});
});
}
/**
* Remove forbidden special chars from the filename
* @param string txt
* @param string txt
*/
function cleanupFilename(txt) {
return txt.replace(/[^a-zA-ZÀ-ÖØ-öø-ÿ0-9,. -]/g, '_').trim().replace(/\.$/, '');
}
/**
* Trigger download of given blob
* @param Blob blob
* @param string filename
*/
function download(blob, filename) {
if(window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, filename);
closeAudioZip();
} else {
// Create link
var a = document.createElement('a');
a.style.display = "none";
document.body.appendChild(a);
// Trigger download blob
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = filename;
a.click();
setTimeout(() => {
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
closeAudioZip();
}, 0);
}
}
function closeAudioZip() {
dialog.classList.remove('visible');
}
setTimeout(function(){
init();
}, 1000);
}
// Add CSS
var css = document.createElement('style');
css.textContent = `
.infodialog {
box-shadow: 0 2px 8px 0 rgba(0,0,0,.2);
border-radius: 2px;
transform: translate3d(0,24px,0);
transition: transform .15s cubic-bezier(0.4,0.0,1,1) ,opacity .15s cubic-bezier(0.4,0.0,1,1) ,visibility 0ms linear .15s;
bottom: 24px;
display: block;
left: auto;
max-height: 323px;
min-width: 300px;
opacity: .01;
overflow: visible;
position: fixed;
right: 24px;
visibility: hidden;
z-index: 1001;
}
.infodialog.visible {
transform: translate3d(0,0,0);
transition-delay: 0;
opacity: 1;
visibility: visible;
}
.infodialog--header {
border-radius: 3px 3px 0 0;
background-color: #323232;
color: #fff;
display: flex;
align-items: center;
font-size: 14px;
height: 52px;
padding: 0 24px;
text-overflow: ellipsis;
white-space: nowrap;
}
.infodialog--about {
font-size: 0.8em;
padding: 10px 0;
padding-left: 24px;
color: #222;
background: #fff;
}
#loadingPDF {
display: none;
border-radius: 50%;
width: 12px;
height: 12px;
border: .12rem solid rgb(0, 0, 0);
border-top-color: white;
animation: spin 1s infinite linear;
margin-right: 8px;
}
#loadingPDF.visible {
display: inline-block;
}
#exportAudio {
margin-right: 5px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}`;
document.head.appendChild(css);
// Inject JS directly in page to prevent limitations of access
var script = document.createElement('script');
script.setAttribute("type", "application/javascript");
script.appendChild(document.createTextNode('('+ main +')();'));
document.body.appendChild(script);