commit 5ac93fee3d332ebb82792968c9a6e96bc8c36f5f
parent 3bde12a2af8e4123ff41729b371808b7838999a7
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Thu, 19 Oct 2023 22:01:39 +0200
feat(core/message_exporter): new template design
Diffstat:
2 files changed, 317 insertions(+), 243 deletions(-)
diff --git a/core/src/main/assets/web/export_template.html b/core/src/main/assets/web/export_template.html
@@ -1,295 +1,362 @@
<style>
:root {
- --sigIconPrimary: #dedede;
- --sigIconSecondary: #999;
- --sigIconTertiary: #616161;
- --sigIconNegative: #f23c57;
- --sigTextPrimary: #dedede;
- --sigTextPrimaryInverse: #000;
- --sigTextSecondary: #999;
- --sigTextTertiary: #616161;
- --sigTextPlayer: #fff;
- --sigTextNegative: #f23c57;
- --sigColorBackgroundBorder: rgba(255, 255, 255, 0.1);
- --sigBackgroundPrimary: #121212;
- --sigBackgroundPrimaryInverse: #fff;
- --sigBackgroundSecondary: #1e1e1e;
- --sigBackgroundSecondaryHover: #2b2b2b;
- --sigBackgroundFeedHover: rgba(255, 255, 255, 0.1);
- --sigBackgroundMessageHover: #292929;
- --sigBackgroundMessageSaved: #333232;
- --sigBackgroundMessageSavedHover: #3a3a3a;
- --sigMediaControlContainerBackground: rgba(255, 255, 255, 0.1);
- --sigStartupFooterBackground: rgba(0, 0, 0, 0.05);
- --sigButtonPrimary: #0fadff;
- --sigButtonPrimaryHover: #42bfff;
- --sigButtonSecondary: #2b2b2b;
- --sigButtonSecondaryHover: #424242;
- --sigButtonSecondaryActive: #5c5c5c;
- --sigButtonTertiary: #4e565f;
- --sigButtonQuaternary: #fff;
- --sigButtonInactive: #1e1e1e;
- --sigButtonNegative: #e1143d;
- --sigButtonOnPrimary: #fff;
- --sigButtonOnSecondary: #dedede;
- --sigButtonOnTertiary: #fff;
- --sigButtonOnQuaternary: #1e1e1e;
- --sigButtonOnInactive: rgba(255, 255, 255, 0.3);
- --sigButtonOnNegative: #fff;
- --sigMain: #121212;
- --sigSubscreen: #121212;
- --sigOverlay: rgba(0, 0, 0, 0.4);
- --sigOverlayHover: rgba(0, 0, 0, 0.35);
- --sigSurface: #1e1e1e;
- --sigSurfaceRGB: 30, 30, 30;
- --sigSurfaceDown: #212121;
- --sigAboveSurface: #292929;
- --sigObject: rgba(255, 255, 255, 0.1);
- --sigObjectDown: rgba(255, 255, 255, 0.19);
- --sigConversationBoxBackground: rgba(255, 255, 255, 0.25);
- --sigDivider: rgba(255, 255, 255, 0.1);
- --sigDividerLight: rgba(255, 255, 255, 0.2);
- --sigPlaceholder: #1e1e1e;
- --sigDisabled: rgba(255, 255, 255, 0.1);
- --sigCallTileHighlight: rgba(255, 255, 255, 0.8);
- --sigChat: #0fadff;
- --sigSnapWithoutSound: #f23c57;
- --sigSnapWithSound: #a05dcd;
- --sigChatSurfaceCalling: #39ca8e;
- --sigChatSurfaceCallingDisabled: #105e3d;
- --sigChatPending: #767676;
- --sigChatPendingHover: #8f8f8f;
- --sigChatIcon: #0fadff;
- --sigChatIconCaret: #f8616d;
- --sigChatShadowOne: 0 0 17px rgba(33, 33, 33, 0.07), 0 0 22px rgba(0, 0, 0, 0.06), 0 0 8px rgba(84, 84, 84, 0.1);
- --selectedMiddleColorGradient: rgba(4, 4, 4, 0.1);
- --selectedRightColorGradient: rgba(4, 4, 4, 0);
+ --Snap-sigIconPrimary: #dedede;
+ --Snap-sigIconSecondary: #999;
+ --Snap-sigIconTertiary: #616161;
+ --Snap-sigIconNegative: #f23c57;
+ --Snap-sigTextPrimary: #dedede;
+ --Snap-sigTextPrimaryInverse: #000;
+ --Snap-sigTextSecondary: #999;
+ --Snap-sigTextTertiary: #616161;
+ --Snap-sigTextPlayer: #fff;
+ --Snap-sigTextNegative: #f23c57;
+ --Snap-sigColorBackgroundBorder: rgba(255, 255, 255, 0.1);
+ --Snap-sigBackgroundPrimary: #121212;
+ --Snap-sigBackgroundPrimaryInverse: #fff;
+ --Snap-sigBackgroundSecondary: #1e1e1e;
+ --Snap-sigBackgroundSecondaryHover: #2b2b2b;
+ --Snap-sigBackgroundFeedHover: rgba(255, 255, 255, 0.1);
+ --Snap-sigBackgroundMessageHover: #292929;
+ --Snap-sigBackgroundMessageSaved: #333232;
+ --Snap-sigBackgroundMessageSavedHover: #3a3a3a;
+ --Snap-sigMediaControlContainerBackground: rgba(255, 255, 255, 0.1);
+ --Snap-sigStartupFooterBackground: rgba(0, 0, 0, 0.05);
+ --Snap-sigButtonPrimary: #0fadff;
+ --Snap-sigButtonPrimaryHover: #42bfff;
+ --Snap-sigButtonSecondary: #2b2b2b;
+ --Snap-sigButtonSecondaryHover: #424242;
+ --Snap-sigButtonSecondaryActive: #5c5c5c;
+ --Snap-sigButtonTertiary: #4e565f;
+ --Snap-sigButtonQuaternary: #fff;
+ --Snap-sigButtonInactive: #1e1e1e;
+ --Snap-sigButtonNegative: #e1143d;
+ --Snap-sigButtonOnPrimary: #fff;
+ --Snap-sigButtonOnSecondary: #dedede;
+ --Snap-sigButtonOnTertiary: #fff;
+ --Snap-sigButtonOnQuaternary: #1e1e1e;
+ --Snap-sigButtonOnInactive: rgba(255, 255, 255, 0.3);
+ --Snap-sigButtonOnNegative: #fff;
+ --Snap-sigMain: #121212;
+ --Snap-sigSubscreen: #121212;
+ --Snap-sigOverlay: rgba(0, 0, 0, 0.4);
+ --Snap-sigOverlayHover: rgba(0, 0, 0, 0.35);
+ --Snap-sigSurface: #1e1e1e;
+ --Snap-sigSurfaceRGB: 30, 30, 30;
+ --Snap-sigSurfaceDown: #212121;
+ --Snap-sigAboveSurface: #292929;
+ --Snap-sigObject: rgba(255, 255, 255, 0.1);
+ --Snap-sigObjectDown: rgba(255, 255, 255, 0.19);
+ --Snap-sigConversationBoxBackground: rgba(255, 255, 255, 0.25);
+ --Snap-sigDivider: rgba(255, 255, 255, 0.1);
+ --Snap-sigDividerLight: rgba(255, 255, 255, 0.2);
+ --Snap-sigPlaceholder: #1e1e1e;
+ --Snap-sigDisabled: rgba(255, 255, 255, 0.1);
+ --Snap-sigCallTileHighlight: rgba(255, 255, 255, 0.8);
+ --Snap-sigChat: #0fadff;
+ --Snap-sigSnapWithoutSound: #f23c57;
+ --Snap-sigSnapWithSound: #a05dcd;
+ --Snap-sigChatSurfaceCalling: #39ca8e;
+ --Snap-sigChatSurfaceCallingDisabled: #105e3d;
+ --Snap-sigChatPending: #767676;
+ --Snap-sigChatPendingHover: #8f8f8f;
+ --Snap-sigChatIcon: #0fadff;
+ --Snap-sigChatIconCaret: #f8616d;
+ --Snap-sigChatShadowOne: 0 0 17px rgba(33, 33, 33, 0.07), 0 0 22px rgba(0, 0, 0, 0.06), 0 0 8px rgba(84, 84, 84, 0.1);
+ --Snap-selectedMiddleColorGradient: rgba(4, 4, 4, 0.1);
+ --Snap-selectedRightColorGradient: rgba(4, 4, 4, 0);
+ --Border: 1px solid var(--Snap-sigColorBackgroundBorder);
}
body {
- margin: 0;
font-family: 'Avenir Next', sans-serif;
- color: var(--sigTextPrimary);
- background-color: var(--sigBackgroundPrimary);
+ color: var(--Snap-sigTextPrimary);
+ background-color: var(--Snap-sigBackgroundPrimary);
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
}
- /* like an header */
- .conversation_summary {
+ header {
+ width: 100%;
+ padding: 10px 0px;
display: flex;
- flex-direction: row;
+ flex-direction: column;
align-items: center;
- padding: 10px;
- border-bottom: 1px solid var(--sigColorBackgroundBorder);
+ justify-content: flex-start;
+ }
+
+ header .title {
+ background-color: var(--Snap-sigButtonSecondary);
+ height: 40px;
+ font-weight: 600;
+ padding-inline-end: 16px;
+ border-radius: 999px;
+ line-height: 40px;
+ padding: 0 20px;
}
- .conversation_message_container {
+ main {
+ background-color: var(--Snap-sigBackgroundSecondary);
+ border: var(--Border);
+ border-radius: 12px;
+ width: calc(100% - 30px);
display: flex;
flex-direction: column;
- padding: 5px;
- background-color: var(--sigBackgroundSecondary);
}
- .message {
+ main>.message {
display: flex;
- flex-direction: row;
- align-items: center;
- padding: 5px;
- margin-left: 5px;
- border-bottom: 1px solid var(--sigColorBackgroundBorder);
+ flex-direction: column;
+ align-items: stretch;
+ justify-content: flex-start;
+ flex-wrap: nowrap;
+ margin: 5px 15px;
}
- .message .header {
+ main>.message .header {
+ width: 100%;
display: flex;
- flex-direction: column;
vertical-align: top;
align-self: flex-start;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ align-items: center;
}
- .message .username {
+ main>.message:nth-child(2n) .username {
+ color: #dcedc1;
+ }
+
+ main>.message:nth-child(2n + 1) .username {
+ color: #ffd3b6;
+ }
+
+ main>.message .username {
font-weight: bold;
}
- .message .time {
+ main>.message .time {
+ color: var(--Snap-sigTextSecondary);
font-size: 12px;
- color: var(--sigTextSecondary);
+ font-weight: 600;
}
- .message .content {
- margin-left: 10px;
- display: flex;
- flex-direction: row;
- align-items: center;
+ main>.message:nth-child(2n) .content {
+ border-color: #dcedc1;
+ }
+ main>.message:nth-child(2n + 1) .content {
+ border-color: #ffd3b6;
}
- .chat_media {
- max-width: 300px;
- max-height: 500px;
+ main>.message .content {
+ background-color: var(--Snap-sigBackgroundMessageSaved);
+ border-left: 3px solid;
+ border-radius: 3px;
+ margin-top: 4px;
+ padding-left: 4px;
+ padding: 3px 0 3px 6px;
}
- .overlay_media {
+ main>.message .content div:has(.chat_media:not(audio):not(.overlay_media)) {
+ display: inline-block;
+ resize: horizontal;
+ overflow: hidden;
+ line-height: 0;
+ height: auto;
+ width: 300px;
+ }
+
+ main>.message .chat_media:not(audio):not(.overlay_media) {
+ width: 100%;
+ height: auto;
+ }
+
+ @-moz-document url-prefix() {
+ main>.message .content div {
+ display: inline-block;
+ resize: horizontal;
+ overflow: hidden;
+ line-height: 0;
+ height: auto;
+ width: 300px;
+ }
+
+ main>.message .chat_media:not(.overlay_media) {
+ width: 100%;
+ height: auto;
+ }
+ }
+
+
+ main>.message .overlay_media {
+ width: inherit;
+ height: inherit;
position: absolute;
pointer-events: none;
}
- .red_snap_svg {
- color: var(--sigSnapWithoutSound);
+ main>.message .red_snap_svg {
+ color: var(--Snap-sigSnapWithoutSound);
}
</style>
<body>
- <div class="conversation_summary">
- <div class="title"></div>
- </div>
-
- <div class="conversation_message_container"></div>
- <div style="display: none;">
- <svg class="red_snap_svg" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <rect x="2.75" y="2.75" width="10.5" height="10.5" rx="1.808" stroke="currentColor" stroke-width="1.5"></rect>
- </svg>
- </div>
-
- <script>
- const conversationData = JSON.parse(document.querySelector(".exported_content").innerHTML)
- const participants = Object.values(conversationData.participants)
-
- function base64decode(data) {
- return new Uint8Array(atob(data).split('').map(c => c.charCodeAt(0)))
- }
+<header>
+ <div class="title"></div>
+</header>
- function makeConversationSummary() {
- const conversationTitle = conversationData.conversationName != null ?
- conversationData.conversationName : "DM with " + Object.values(participants).map(user => user.username).join(", ")
+<main></main>
- document.querySelector(".conversation_summary .title").textContent = conversationTitle
- }
+<div style="display: none;">
+ <svg class="red_snap_svg" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <rect x="4" y="5" width="10.5" height="10.5" rx="1.808" stroke="currentColor" stroke-width="1.5"></rect>
+ </svg>
+</div>
- function decodeMedia(element) {
- const decodedData = new Uint8Array(
- inflate(
- base64decode(
- element.innerHTML.substring(5, element.innerHTML.length - 4)
- )
+<script>
+ function base64decode(data) {
+ return new Uint8Array(atob(data).split('').map(c => c.charCodeAt(0)))
+ }
+
+ const conversationData = JSON.parse(new TextDecoder().decode(new Uint8Array(inflate(base64decode(document.querySelector(".exported_content").innerHTML)))))
+ const participants = Object.values(conversationData.participants)
+
+ function makeHeader() {
+ const conversationTitle = conversationData.conversationName != null ? conversationData.conversationName : "DM with " + Object.values(participants).map(user => user.username).join(", ")
+ document.querySelector("header > .title").textContent = conversationTitle
+ document.title = conversationTitle
+ }
+
+ function decodeMedia(element) {
+ const decodedData = new Uint8Array(
+ inflate(
+ base64decode(
+ element.innerHTML.substring(5, element.innerHTML.length - 4)
)
)
+ )
- return URL.createObjectURL(new Blob([decodedData]))
- }
-
- function makeConversationMessageContainer() {
- const messageTemplate = document.querySelector("#message_template")
- Object.values(conversationData.messages).forEach(message => {
- const messageObject = document.createElement("div")
- messageObject.classList.add("message")
+ return URL.createObjectURL(new Blob([decodedData]))
+ }
- messageObject.appendChild(((headerElement) =>{
- headerElement.classList.add("header")
+ function makeMain() {
+ const messageTemplate = document.querySelector("#message_template")
+ Object.values(conversationData.messages).forEach(message => {
+ const messageObject = document.createElement("div")
+ messageObject.classList.add("message")
- headerElement.appendChild(((elem) =>{
- elem.classList.add("username")
- const participant = participants[message.senderId]
- elem.innerHTML = (participant == null) ? "Unknown user" : participant.username
- return elem
- })(document.createElement("div")))
+ messageObject.appendChild(((headerElement) => {
+ headerElement.classList.add("header")
+ headerElement.appendChild(((elem) => {
+ elem.classList.add("username")
+ const participant = participants[message.senderId]
+ elem.innerHTML = (participant == null) ? "Unknown user" : participant.username
+ return elem
+ })(document.createElement("div")))
- headerElement.appendChild(((elem) =>{
- elem.classList.add("time")
- elem.innerHTML = new Date(message.createdTimestamp).toISOString()
- return elem
- })(document.createElement("div")))
- return headerElement
+ headerElement.appendChild(((elem) => {
+ elem.classList.add("time")
+ elem.innerHTML = new Date(message.createdTimestamp).toUTCString()
+ return elem
})(document.createElement("div")))
- messageObject.appendChild(((messageContainer) =>{
- messageContainer.classList.add("content")
-
- messageContainer.innerHTML = message.serializedContent
-
- if (!message.serializedContent) {
- messageContainer.innerHTML = ""
- let messageData = ""
- switch(message.type) {
- case "SNAP":
- messageContainer.appendChild(document.querySelector('.red_snap_svg').cloneNode(true))
- messageData = "Snap"
- break
- default:
- messageData = message.type
- }
- messageContainer.innerHTML += messageData
+ return headerElement
+ })(document.createElement("div")))
+
+ messageObject.appendChild(((messageContainer) => {
+ messageContainer.classList.add("content")
+ messageContainer.innerHTML = message.serializedContent
+
+ if (!message.serializedContent) {
+ messageContainer.innerHTML = ""
+ let messageData = ""
+ switch (message.type) {
+ case "SNAP":
+ messageContainer.appendChild(document.querySelector('.red_snap_svg').cloneNode(true))
+ messageData = "Snap"
+ break
+ default:
+ messageData = message.type
}
+ messageContainer.innerHTML += messageData
+ }
- if (message.attachments && message.attachments.length > 0) {
- let observers = []
+ if (message.attachments && message.attachments.length > 0) {
+ let observers = []
- message.attachments.forEach((attachment, index) => {
- const mediaKey = attachment.key.replace(/(=)/g, "")
+ message.attachments.forEach((attachment, index) => {
+ const mediaKey = attachment.key.replace(/(=)/g, "")
- observers.push(() => {
- const originalMedia = document.querySelector('.media-ORIGINAL_' + mediaKey)
- if (!originalMedia) {
- return
- }
+ observers.push(() => {
+ const originalMedia = document.querySelector('.media-ORIGINAL_' + mediaKey)
+ if (!originalMedia) {
+ return
+ }
- const originalMediaUrl = decodeMedia(originalMedia)
+ const originalMediaUrl = decodeMedia(originalMedia)
- const mediaContainer = document.createElement("div")
- messageContainer.appendChild(mediaContainer)
+ const mediaContainer = document.createElement("div")
+ messageContainer.appendChild(mediaContainer)
- const imageTag = document.createElement("img")
- imageTag.src = originalMediaUrl
- imageTag.classList.add("chat_media")
- mediaContainer.appendChild(imageTag)
+ const imageTag = document.createElement("img")
+ imageTag.src = originalMediaUrl
+ imageTag.classList.add("chat_media")
+ mediaContainer.appendChild(imageTag)
- imageTag.onerror = () => {
- mediaContainer.removeChild(imageTag)
- const mediaTag = document.createElement(message.type === "NOTE" ? "audio" : "video")
- mediaTag.classList.add("chat_media")
- mediaTag.src = originalMediaUrl
- mediaTag.preload = "metadata"
- mediaTag.controls = true
- mediaContainer.appendChild(mediaTag)
- }
+ imageTag.onerror = () => {
+ mediaContainer.removeChild(imageTag)
+ const mediaTag = document.createElement(message.type === "NOTE" ? "audio" : "video")
+ mediaTag.classList.add("chat_media")
+ mediaTag.src = originalMediaUrl
+ mediaTag.preload = "metadata"
+ mediaTag.controls = true
+ mediaContainer.appendChild(mediaTag)
+ }
- const overlay = document.querySelector('.media-OVERLAY_' + mediaKey)
- if (!overlay) {
- return
- }
+ const overlay = document.querySelector('.media-OVERLAY_' + mediaKey)
+ if (!overlay) {
+ return
+ }
- const overlayImage = document.createElement("img")
- overlayImage.src = decodeMedia(overlay)
- overlayImage.classList.add("chat_media")
- overlayImage.classList.add("overlay_media")
- mediaContainer.appendChild(overlayImage)
- })
+ const overlayImage = document.createElement("img")
+ overlayImage.src = decodeMedia(overlay)
+ overlayImage.classList.add("chat_media")
+ overlayImage.classList.add("overlay_media")
+ mediaContainer.appendChild(overlayImage)
})
+ })
+
+ let fetched = false
+
+ new IntersectionObserver(entries => {
+ if (!fetched && entries[0].isIntersecting === true) {
+ fetched = true
+ messageContainer.innerHTML = ""
+ observers.forEach(c => {
+ try {
+ c()
+ } catch (e) {
+ console.log(e)
+ }
+ })
+ }
+ }).observe(messageContainer)
+ }
- let fetched = false
-
- new IntersectionObserver(entries => {
- if(!fetched && entries[0].isIntersecting === true) {
- fetched = true
- messageContainer.innerHTML = ""
- observers.forEach(c => {
- try {
- c()
- } catch (e) {
- console.log(e)
- }
- })
- }
- }).observe(messageContainer)
- }
-
- return messageContainer
- })(document.createElement("div")))
+ return messageContainer
+ })(document.createElement("div")))
- document.querySelector('.conversation_message_container').appendChild(messageObject)
- })
- }
+ document.querySelector('main').appendChild(messageObject)
+ })
+ }
- makeConversationSummary()
- makeConversationMessageContainer()
- </script>
-</body>
+ makeHeader()
+ makeMain()
+</script>
+</body>+
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageExporter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageExporter.kt
@@ -2,6 +2,7 @@ package me.rhunk.snapenhance.core.messaging
import android.os.Environment
import android.util.Base64InputStream
+import android.util.Base64OutputStream
import com.google.gson.JsonArray
import com.google.gson.JsonNull
import com.google.gson.JsonObject
@@ -21,11 +22,7 @@ import me.rhunk.snapenhance.core.features.impl.downloader.decoder.AttachmentType
import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder
import me.rhunk.snapenhance.core.wrapper.impl.Message
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
-import java.io.BufferedInputStream
-import java.io.File
-import java.io.FileOutputStream
-import java.io.InputStream
-import java.io.OutputStream
+import java.io.*
import java.text.SimpleDateFormat
import java.util.Collections
import java.util.Date
@@ -34,6 +31,7 @@ import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.zip.Deflater
import java.util.zip.DeflaterInputStream
+import java.util.zip.DeflaterOutputStream
import java.util.zip.ZipFile
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@@ -172,17 +170,15 @@ class MessageExporter(
mediaFiles.forEach { (key, filePair) ->
output.write("<div class=\"media-$key\"><!-- ".toByteArray())
-
- val deflateInputStream = DeflaterInputStream(filePair.second.inputStream(), Deflater(Deflater.BEST_COMPRESSION, true))
- val base64InputStream = XposedHelpers.newInstance(
- Base64InputStream::class.java,
- deflateInputStream,
- android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP,
- true
- ) as InputStream
- base64InputStream.copyTo(output)
- deflateInputStream.close()
-
+ filePair.second.inputStream().use { inputStream ->
+ val deflateInputStream = DeflaterInputStream(inputStream, Deflater(Deflater.BEST_COMPRESSION, true))
+ (XposedHelpers.newInstance(
+ Base64InputStream::class.java,
+ deflateInputStream,
+ android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP,
+ true
+ ) as InputStream).copyTo(output)
+ }
output.write(" --></div>\n".toByteArray())
output.flush()
updateProgress("wrote")
@@ -191,7 +187,17 @@ class MessageExporter(
//write the json file
output.write("<script type=\"application/json\" class=\"exported_content\">".toByteArray())
- exportJson(output)
+
+ val deflateOutputStream = DeflaterOutputStream((XposedHelpers.newInstance(
+ Base64OutputStream::class.java,
+ output,
+ android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP,
+ true
+ ) as OutputStream), Deflater(Deflater.BEST_COMPRESSION, true))
+
+ exportJson(deflateOutputStream)
+ deflateOutputStream.finish()
+
output.write("</script>\n".toByteArray())
printLog("writing template...")