NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Google Slides - Offline Presentation // @description It allows you to save a Google Slides presentation as a working html file using the browser's "complete webpage save" feature // @version 1 // @grant none // @include https://docs.google.com/presentation/d/e/*/pub* // @include https://docs.google.com/presentation/d/*/present* // @namespace https://greasyfork.org/users/396351 // @license MIT // ==/UserScript== // Utils const loadImage = src => { const img = document.createElement('img'); img.src = src; img.style.display = 'none'; document.body.appendChild(img); } const getFilename = path => path.split('/').slice(-1)[0]; // Load slide images as img tags so that they're saved const viewerData = window.eval('viewerData'); [].concat.apply(this, viewerData.docData[1]).filter(a => Array.isArray(a) && a.length > 0).flatMap(a => a).filter(h => h.startsWith && h.startsWith('https')).forEach(async h => { loadImage(h); const scripts = Array.from(document.querySelectorAll('script')); const svgs = scripts.filter(s => s.innerHTML.startsWith('SK_svgData')); const type = await fetch(h, {method: 'HEAD'}).then(r => r.headers.get('Content-Type').split('/')[1]); const local = `' + localStorage.getItem('loc') + '/${getFilename(h).substring(0, 60)}.${type.replace('jpeg', 'jpg')}`; const escapedLink = h.replace(/\//g, '\\/'); svgs.forEach(s => s.innerHTML = s.innerHTML.replace(escapedLink, local)); const vdScript = scripts.filter(s => s.innerHTML.includes('var viewerData'))[0]; vdScript.innerHTML = vdScript.innerHTML.replace(new RegExp(h, 'g'), local.replace(/'/g, '"')); }); // Load button images as img tags const [viewerStyle, style, media] = Array.from(document.styleSheets[0].cssRules).filter(r => { return r.cssText.match(/punch_viewer_sprite|\/viewer-/) && r.cssText.includes('nav'); }); const mediaStyle = Array.from(media.cssRules).filter(r => r.cssText.includes('sprite'))[0]; const [viewer, sprite, mediaSprite] = [viewerStyle, style, mediaStyle].map(s => getFilename(s.style['background-image'].slice(4, -2))); const staticImageURL = 'https://ssl.gstatic.com/docs/presentations/images/'; loadImage(staticImageURL + mediaSprite); loadImage(staticImageURL + sprite); loadImage(staticImageURL + viewer) // Function whose source to load as a script tag const source = () => { // Remove overlay document.addEventListener('DOMContentLoaded', (e) => { document.querySelectorAll('.punch-viewer-container')[1].parentNode.remove(); }); window.addEventListener('load', e => { if (!location.href.startsWith('file')) return; // Hack to find the location of the local file const loc = Array.from(document.querySelectorAll('link')).filter(l => l.rel == 'stylesheet')[0].getAttribute('href').split('/')[0]; const prevLoc = localStorage.getItem('loc'); if (!prevLoc || prevLoc != loc) { localStorage.setItem('loc', loc); location.reload(); } //Fix main window styling const style = document.createElement('style'); style.type = 'text/css'; style.innerHTML =` .punch-viewer-nav-v2 .punch-viewer-nav-logo-image, .punch-viewer-nav-v2 .punch-viewer-icon, .punch-viewer-nav-v2 .punch-viewer-icon-large, .punch-viewer-nav-v2 .punch-viewer-laser-icon, .punch-viewer-nav-v2 .goog-flat-menu-button-dropdown, .punch-viewer-body-v2 .goog-option-selected .goog-menuitem-checkbox, .punch-viewer-body-v2 .punch-viewer-menuitem-skippedslide .goog-menuitem-checkbox, .lmwd-dialog .punch-viewer-icon, .lmwd-dialog .punch-viewer-icon-large { background-image:url(${loc}/${sprite}) } @media screen and (min-resolution:2dppx),(-webkit-min-device-pixel-ratio:2) { .punch-viewer-nav-v2 .punch-viewer-nav-logo-image, .punch-viewer-nav-v2 .punch-viewer-icon, .punch-viewer-nav-v2 .punch-viewer-icon-large, .punch-viewer-nav-v2 .punch-viewer-laser-icon, .punch-viewer-nav-v2 .goog-flat-menu-button-dropdown, .punch-viewer-body-v2 .goog-option-selected .goog-menuitem-checkbox, .punch-viewer-body-v2 .punch-viewer-menuitem-skippedslide .goog-menuitem-checkbox, .lmwd-dialog .punch-viewer-icon, .lmwd-dialog .punch-viewer-icon-large { background-image:url(${loc}/${mediaSprite}); } } `; document.head.appendChild(style); // Fix speaker notes window styling window._open = window.open; window.open = function(){ const win = window._open.apply(this, arguments); const $ = (selector) => win.document.querySelector(selector); const link = document.querySelector('link[href*=viewer_css_ltr]').cloneNode(); win.document._write = win.document.write; win.document.write = function() { const val = win.document._write.apply(this, arguments); win.document.head.appendChild(link); setTimeout(() => { const sidePanelLength = 331; const dragger = '.punch-viewer-speakernotes-dragger'; const drag = () => { const rect = $(dragger).getBoundingClientRect(); const evArgs = { buttons:1, clientX: rect.x + rect.width/2, clientY: 100, bubbles: true }; const mouse = (selector, type, args) => $(selector).dispatchEvent(new MouseEvent('mouse' + type, args)); mouse(dragger, 'down', evArgs); mouse('.punch-viewer-speakernotes-text-body-scrollable', 'move', {...evArgs, clientX: sidePanelLength, movementX: 1}); mouse('.punch-viewer-speakernotes-dragger', 'up', {...evArgs, clientX: sidePanelLength}); } drag(); drag(); Array.from(win.document.querySelectorAll('.punch-viewer-speakernotes-page-iframe')).forEach(i => { i.contentDocument.head.appendChild(link.cloneNode()); }); $('#punch-viewer-speakernotes').addEventListener('mousemove', e => { const currentSidePanelLength = parseInt($('.punch-viewer-speakernotes-side-panel').style.width.slice(0, -2)); if(e.buttons == 1 && (e.movementX <= 0 && currentSidePanelLength <= sidePanelLength || e.clientX <= sidePanelLength)) { e.stopPropagation(); } }, true); const style = win.document.createElement('style'); style.type = 'text/css'; style.innerHTML = ` .punch-viewer-nav-logo-image, .punch-viewer-icon, .punch-viewer-icon-large, .punch-viewer-laser-icon, .punch-viewer-nav .goog-flat-menu-button-dropdown { background-image:url(${win.localStorage.getItem('loc')}/${viewer}) } `; win.document.head.appendChild(style); }, 500); return val; } return win; } }); } const extractVariables = vars => Object.keys(vars).map(name => { return `${name} = ${vars[name].toSource()}`; }).join(';') + ';'; const script = document.createElement('script'); script.type = 'text/javascript'; const scriptSource = source.toSource().split('\n').slice(1, -1).join('\n') script.innerHTML = extractVariables({viewer, sprite, mediaSprite}) + scriptSource;