/* Copyright (c) 2020 Thomas Weber This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ window.Packager = (function() { const readAsDataURL = (blob) => new Promise((resolve, reject) => { const fr = new FileReader(); fr.onload = () => resolve(fr.result); fr.onerror = () => reject('could not read as data url'); fr.readAsDataURL(blob); }); const readAsArrayBuffer = (blob) => new Promise((resolve, reject) => { const fr = new FileReader(); fr.onload = () => resolve(fr.result); fr.onerror = () => reject('could not read as arraybuffer'); fr.readAsArrayBuffer(blob); }); const loadImage = (src) => new Promise((resolve, reject) => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error(`Could not load image: ${src}`)); image.src = src; }); const canvasToBlob = (canvas) => new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject('Could not read as blob'); } }); // png is default type, quality is ignored }); class Project { constructor(blob, type) { this.blob = blob; this.type = type; } async asDataURL() { return { data: await readAsDataURL(this.blob), type: this.type }; } async asFetchedFrom(path) { return { data: path, type: this.type }; } } class ScriptOrStyleLoader { constructor(files, pathPrefix = '') { this.files = files; this.pathPrefix = pathPrefix; } async _loadInlineSource(source) { const response = await fetch(this.pathPrefix + source); const blob = await response.blob(); const url = await readAsDataURL(blob); return url; } async _loadFile(file) { const response = await fetch(this.pathPrefix + file.src); let body = await response.text(); if (file.inlineSources) { for (const source of file.inlineSources) { const sourceData = await this._loadInlineSource(source); // string.replace only does the first occurrence, but a source may appear multiple times in the file while (body.includes(source)) { body = body.replace(source, sourceData); } } } file.loaded = true; file.content = body; } async load() { const missingFiles = this.files.filter((i) => !i.loaded); if (missingFiles.length > 0) { await Promise.all(missingFiles.map((i) => this._loadFile(i))); } return this.files.map((i) => i.content).join('\n'); } } class AssetLoader { constructor(files, pathPrefix = '') { this.files = files; this.pathPrefix = pathPrefix; } async _loadAsset(asset) { const response = await fetch(this.pathPrefix + asset.src); const blob = await response.blob(); const data = await readAsDataURL(blob); asset.loaded = true; asset.blob = blob; asset.data = data; } async load() { const missing = this.files.filter((i) => !i.loaded); if (missing.length > 0) { await Promise.all(missing.map((i) => this._loadAsset(i))); } return this.files; } } class TurboWarp { async package(projectData) { const script = await TurboWarp.scriptLoader.load(); return ` `; } } TurboWarp.scriptLoader = new ScriptOrStyleLoader([ { src: 'player.js' } ], 'https://packagerdata.turbowarp.org/'); class Forkphorus { constructor(options) { this.playerOptions = options.playerOptions; this.controlsOptions = options.controlsOptions; this.loadingScreenText = options.loadingScreenText; } async package(projectData) { const [ scripts, styles, assets ] = await Promise.all([ Forkphorus.scriptLoader.load(), Forkphorus.styleLoader.load(), Forkphorus.assetLoader.load(), ]); return `
${this.loadingScreenText ? `

${this.loadingScreenText}

` : ''}

Internal Error

`; } } Forkphorus.scriptLoader = new ScriptOrStyleLoader([ { src: 'lib/scratch-sb1-converter.js', }, { src: 'lib/canvg.min.js', }, { src: 'lib/fontfaceobserver.standalone.js', }, { src: 'lib/jszip.min.js', }, { src: 'lib/rgbcolor.js', }, { src: 'lib/stackblur.min.js', }, { src: 'phosphorus.dist.js', inlineSources: ['icons.svg'] }, ], 'https://forkphorus.github.io/'); Forkphorus.styleLoader = new ScriptOrStyleLoader([ { src: 'phosphorus.css', inlineSources: [ 'icons.svg', 'icons/click-to-play.svg', 'fonts/DonegalOne-Regular.woff', 'fonts/GloriaHallelujah.woff', 'fonts/MysteryQuest-Regular.woff', 'fonts/PermanentMarker-Regular.woff', ] } ], 'https://forkphorus.github.io/'); Forkphorus.assetLoader = new AssetLoader([ { src: 'soundbank/sb2/instruments/AcousticGuitar_F3_22k.wav', }, { src: 'soundbank/sb2/instruments/AcousticPiano(5)_A%233_22k.wav', }, { src: 'soundbank/sb2/instruments/AcousticPiano(5)_C4_22k.wav', }, { src: 'soundbank/sb2/instruments/AcousticPiano(5)_G4_22k.wav', }, { src: 'soundbank/sb2/instruments/AcousticPiano(5)_F5_22k.wav', }, { src: 'soundbank/sb2/instruments/AcousticPiano(5)_C6_22k.wav', }, { src: 'soundbank/sb2/instruments/AcousticPiano(5)_D%236_22k.wav', }, { src: 'soundbank/sb2/instruments/AcousticPiano(5)_D7_22k.wav', }, { src: 'soundbank/sb2/instruments/AltoSax_A3_22K.wav', }, { src: 'soundbank/sb2/instruments/AltoSax(3)_C6_22k.wav', }, { src: 'soundbank/sb2/instruments/Bassoon_C3_22k.wav', }, { src: 'soundbank/sb2/instruments/BassTrombone_A2(2)_22k.wav', }, { src: 'soundbank/sb2/instruments/BassTrombone_A2(3)_22k.wav', }, { src: 'soundbank/sb2/instruments/Cello(3b)_C2_22k.wav', }, { src: 'soundbank/sb2/instruments/Cello(3)_A%232_22k.wav', }, { src: 'soundbank/sb2/instruments/Choir(4)_F3_22k.wav', }, { src: 'soundbank/sb2/instruments/Choir(4)_F4_22k.wav', }, { src: 'soundbank/sb2/instruments/Choir(4)_F5_22k.wav', }, { src: 'soundbank/sb2/instruments/Clarinet_C4_22k.wav', }, { src: 'soundbank/sb2/instruments/ElectricBass(2)_G1_22k.wav', }, { src: 'soundbank/sb2/instruments/ElectricGuitar(2)_F3(1)_22k.wav', }, { src: 'soundbank/sb2/instruments/ElectricPiano_C2_22k.wav', }, { src: 'soundbank/sb2/instruments/ElectricPiano_C4_22k.wav', }, { src: 'soundbank/sb2/instruments/EnglishHorn(1)_D4_22k.wav', }, { src: 'soundbank/sb2/instruments/EnglishHorn(1)_F3_22k.wav', }, { src: 'soundbank/sb2/instruments/Flute(3)_B5(1)_22k.wav', }, { src: 'soundbank/sb2/instruments/Flute(3)_B5(2)_22k.wav', }, { src: 'soundbank/sb2/instruments/Marimba_C4_22k.wav', }, { src: 'soundbank/sb2/instruments/MusicBox_C4_22k.wav', }, { src: 'soundbank/sb2/instruments/Organ(2)_G2_22k.wav', }, { src: 'soundbank/sb2/instruments/Pizz(2)_A3_22k.wav', }, { src: 'soundbank/sb2/instruments/Pizz(2)_E4_22k.wav', }, { src: 'soundbank/sb2/instruments/Pizz(2)_G2_22k.wav', }, { src: 'soundbank/sb2/instruments/SteelDrum_D5_22k.wav', }, { src: 'soundbank/sb2/instruments/SynthLead(6)_C4_22k.wav', }, { src: 'soundbank/sb2/instruments/SynthLead(6)_C6_22k.wav', }, { src: 'soundbank/sb2/instruments/SynthPad(2)_A3_22k.wav', }, { src: 'soundbank/sb2/instruments/SynthPad(2)_C6_22k.wav', }, { src: 'soundbank/sb2/instruments/TenorSax(1)_C3_22k.wav', }, { src: 'soundbank/sb2/instruments/Trombone_B3_22k.wav', }, { src: 'soundbank/sb2/instruments/Trumpet_E5_22k.wav', }, { src: 'soundbank/sb2/instruments/Vibraphone_C3_22k.wav', }, { src: 'soundbank/sb2/instruments/Violin(2)_D4_22K.wav', }, { src: 'soundbank/sb2/instruments/Violin(3)_A4_22k.wav', }, { src: 'soundbank/sb2/instruments/Violin(3b)_E5_22k.wav', }, { src: 'soundbank/sb2/instruments/WoodenFlute_C5_22k.wav', }, { src: 'soundbank/sb2/drums/BassDrum(1b)_22k.wav', }, { src: 'soundbank/sb2/drums/Bongo_22k.wav', }, { src: 'soundbank/sb2/drums/Cabasa(1)_22k.wav', }, { src: 'soundbank/sb2/drums/Clap(1)_22k.wav', }, { src: 'soundbank/sb2/drums/Claves(1)_22k.wav', }, { src: 'soundbank/sb2/drums/Conga(1)_22k.wav', }, { src: 'soundbank/sb2/drums/Cowbell(3)_22k.wav', }, { src: 'soundbank/sb2/drums/Crash(2)_22k.wav', }, { src: 'soundbank/sb2/drums/Cuica(2)_22k.wav', }, { src: 'soundbank/sb2/drums/GuiroLong(1)_22k.wav', }, { src: 'soundbank/sb2/drums/GuiroShort(1)_22k.wav', }, { src: 'soundbank/sb2/drums/HiHatClosed(1)_22k.wav', }, { src: 'soundbank/sb2/drums/HiHatOpen(2)_22k.wav', }, { src: 'soundbank/sb2/drums/HiHatPedal(1)_22k.wav', }, { src: 'soundbank/sb2/drums/Maracas(1)_22k.wav', }, { src: 'soundbank/sb2/drums/SideStick(1)_22k.wav', }, { src: 'soundbank/sb2/drums/SnareDrum(1)_22k.wav', }, { src: 'soundbank/sb2/drums/Tambourine(3)_22k.wav', }, { src: 'soundbank/sb2/drums/Tom(1)_22k.wav', }, { src: 'soundbank/sb2/drums/Triangle(1)_22k.wav', }, { src: 'soundbank/sb2/drums/Vibraslap(1)_22k.wav', }, { src: 'soundbank/sb2/drums/WoodBlock(1)_22k.wav', }, { src: 'fonts/Knewave-Regular.woff', }, { src: 'fonts/Handlee-Regular.woff', }, { src: 'fonts/Grand9K-Pixel.ttf', }, { src: 'fonts/Griffy-Regular.woff', }, { src: 'fonts/SourceSerifPro-Regular.woff', }, { src: 'fonts/NotoSans-Regular.woff', }, ], 'https://forkphorus.github.io/'); class HTML { async package(runtime, projectData) { const packagerData = await runtime.package(await projectData.asDataURL()); return { data: packagerData, filename: 'project.html', }; } } class Zip { async package(runtime, projectData) { const zip = new JSZip(); const packagerData = await runtime.package(await projectData.asFetchedFrom('project.zip')); zip.file('index.html', packagerData); zip.file('project.zip', projectData.blob); return { data: await zip.generateAsync({ type: 'arraybuffer', compression: 'DEFLATE' }), filename: 'project.zip', }; } } const pngToAppleICNS = async (pngData) => { // Read the Image. const url = URL.createObjectURL(pngData); const image = await loadImage(url); if (image.width !== image.height) { throw new Error('Image width and height do not match'); } const size = image.width; // Determine the formats to create const eligibleFormats = pngToAppleICNS.formats.filter((format) => { // Always include the smallest size so that tiny images will get at least 1 image. if (format.size === 16) { return true; } return size >= format.size; }); // Create a single canvas to be used for conversion // Creating many canvases is prone to error in Safari const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('cannot get canvas rendering context'); } const icns = new Icns.Icns(); for (const format of eligibleFormats) { // Use the canvas to scale the image. const formatSize = format.size; canvas.width = formatSize; canvas.height = formatSize; ctx.drawImage(image, 0, 0, formatSize, formatSize); // Read the data off of the canvas const blob = await canvasToBlob(canvas); const arrayBuffer = await readAsArrayBuffer(blob); // The Icns library wants a Node Buffer, not an ArrayBuffer, so use the exposed Buffer to convert. const buffer = Icns.Buffer.from(arrayBuffer); const icnsImage = await Icns.IcnsImage.fromPNG(buffer, format.type); icns.append(icnsImage); } return icns.data; }; pngToAppleICNS.formats = [ { type: 'ic04', size: 16 }, { type: 'ic07', size: 128 }, { type: 'ic08', size: 256 }, { type: 'ic09', size: 512 }, { type: 'ic10', size: 1024 }, { type: 'ic11', size: 32 }, { type: 'ic12', size: 64 }, { type: 'ic13', size: 256 }, { type: 'ic14', size: 512 }, ]; class NWjs { constructor({ platform, manifest, icon }) { this.platform = platform; this.manifest = manifest; this.icon = icon; } async package(runtime, projectData) { const packagerData = await runtime.package(await projectData.asFetchedFrom('project.zip')); const response = await fetch(`https://packagerdata.turbowarp.org/nwjs-v0.49.0-${this.platform === 'windows' ? 'win' : 'osx'}-x64.zip`); const nwjsData = await response.arrayBuffer(); const nwjsZip = await JSZip.loadAsync(nwjsData); // Inside the nw.js zip is another folder called something like nwjs-v0.48.2-win-x64 // The data we care about is inside this folder // To extract it, we'll take a file name and grab the first part of the path. const nwjsPrefix = Object.keys(nwjsZip.files)[0].split('/')[0]; const zip = nwjsZip.folder(nwjsPrefix); let dataPrefix = ''; if (this.platform === 'mac') { dataPrefix = 'nwjs.app/Contents/resources/app.nw/'; const icnsData = await pngToAppleICNS(this.icon); zip.file('nwjs.app/Contents/Resources/app.icns', icnsData); // TODO: rename nwjs.app and such to reflect the app's name } zip.file(dataPrefix + 'project.html', packagerData); zip.file(dataPrefix + 'project.zip', projectData.blob); zip.file(dataPrefix + 'icon.png', this.icon); zip.file(dataPrefix + 'package.json', this.manifest); return { data: await zip.generateAsync({ type: 'arraybuffer', compression: 'DEFLATE', // Use UNIX permissions so that executable bits are properly set, which matters for macOS platform: 'UNIX', }), filename: 'nwjs.zip', }; } } return { Project, runtimes: { TurboWarp, Forkphorus, }, environments: { HTML, Zip, NWjs, }, }; }());