Finish implementing uhabits-core/jsMain
This commit is contained in:
parent
61d8f358eb
commit
aa9bd1082b
@ -81,6 +81,7 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(npm("sql.js", "1.11.0"))
|
||||
implementation(npm("sprintf-js", "1.1.3"))
|
||||
implementation(npm("jszip", "3.10.1"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker 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.
|
||||
*
|
||||
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.platform
|
||||
|
||||
expect fun <T> runSuspend(block: suspend () -> T): T
|
||||
@ -19,6 +19,12 @@
|
||||
|
||||
package org.isoron.platform.io
|
||||
|
||||
class ZipEntry(val name: String, val content: String)
|
||||
|
||||
expect class ZipReader(bytes: ByteArray) {
|
||||
suspend fun entries(): List<ZipEntry>
|
||||
}
|
||||
|
||||
expect class ZipWriter() {
|
||||
fun addEntry(name: String, content: String)
|
||||
suspend fun toBytes(): ByteArray
|
||||
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker 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.
|
||||
*
|
||||
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.io
|
||||
|
||||
class ZipEntry(val name: String, val content: String)
|
||||
|
||||
expect class ZipReader(bytes: ByteArray) {
|
||||
fun entries(): List<ZipEntry>
|
||||
}
|
||||
@ -20,7 +20,9 @@ package org.isoron.uhabits.core.tasks
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ -31,6 +33,7 @@ class CoroutineTaskRunner(
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + mainDispatcher)
|
||||
private val listeners = mutableListOf<TaskRunner.Listener>()
|
||||
private val jobs = mutableListOf<Job>()
|
||||
private var activeCount = 0
|
||||
|
||||
override val activeTaskCount: Int get() = activeCount
|
||||
@ -45,7 +48,7 @@ class CoroutineTaskRunner(
|
||||
|
||||
override fun execute(task: Task) {
|
||||
task.onAttached(this)
|
||||
scope.launch {
|
||||
val job = scope.launch {
|
||||
activeCount++
|
||||
listeners.forEach { it.onTaskStarted(task) }
|
||||
task.onPreExecute()
|
||||
@ -58,6 +61,13 @@ class CoroutineTaskRunner(
|
||||
activeCount--
|
||||
listeners.forEach { it.onTaskFinished(task) }
|
||||
}
|
||||
job.invokeOnCompletion { jobs.remove(job) }
|
||||
jobs.add(job)
|
||||
}
|
||||
|
||||
override suspend fun await() {
|
||||
jobs.joinAll()
|
||||
jobs.clear()
|
||||
}
|
||||
|
||||
override fun publishProgress(task: Task, progress: Int) {
|
||||
|
||||
@ -29,4 +29,6 @@ interface TaskRunner {
|
||||
fun onTaskStarted(task: Task)
|
||||
fun onTaskFinished(task: Task)
|
||||
}
|
||||
|
||||
suspend fun await()
|
||||
}
|
||||
|
||||
@ -89,9 +89,7 @@ open class ListHabitsBehavior(
|
||||
if (filename != null) {
|
||||
screen.showSendFileScreen(filename)
|
||||
} else {
|
||||
screen.showMessage(
|
||||
Message.COULD_NOT_EXPORT
|
||||
)
|
||||
screen.showMessage(Message.COULD_NOT_EXPORT)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -19,17 +19,15 @@
|
||||
|
||||
package org.isoron.platform.io
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ZipWriterTest {
|
||||
class ZipTest {
|
||||
|
||||
@Test
|
||||
fun testSingleEntry() = runBlocking {
|
||||
fun testSingleEntry() = runTest {
|
||||
val zip = ZipWriter()
|
||||
zip.addEntry("hello.txt", "Hello, World!")
|
||||
val bytes = zip.toBytes()
|
||||
@ -40,7 +38,7 @@ class ZipWriterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleEntries() = runBlocking {
|
||||
fun testMultipleEntries() = runTest {
|
||||
val zip = ZipWriter()
|
||||
zip.addEntry("a.csv", "name,value\nfoo,1\n")
|
||||
zip.addEntry("subdir/b.csv", "col1,col2\nbar,2\n")
|
||||
@ -55,7 +53,7 @@ class ZipWriterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmptyContent() = runBlocking {
|
||||
fun testEmptyContent() = runTest {
|
||||
val zip = ZipWriter()
|
||||
zip.addEntry("empty.txt", "")
|
||||
val bytes = zip.toBytes()
|
||||
@ -66,7 +64,7 @@ class ZipWriterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLargeContent() = runBlocking {
|
||||
fun testLargeContent() = runTest {
|
||||
val zip = ZipWriter()
|
||||
val large = "x".repeat(100_000)
|
||||
zip.addEntry("large.txt", large)
|
||||
@ -77,16 +75,7 @@ class ZipWriterTest {
|
||||
assertEquals(large, entries["large.txt"])
|
||||
}
|
||||
|
||||
private fun readZipEntries(bytes: ByteArray): Map<String, String> {
|
||||
val result = mutableMapOf<String, String>()
|
||||
val zis = ZipInputStream(ByteArrayInputStream(bytes))
|
||||
var entry = zis.nextEntry
|
||||
while (entry != null) {
|
||||
result[entry.name] = zis.readBytes().decodeToString()
|
||||
zis.closeEntry()
|
||||
entry = zis.nextEntry
|
||||
}
|
||||
zis.close()
|
||||
return result
|
||||
private suspend fun readZipEntries(bytes: ByteArray): Map<String, String> {
|
||||
return ZipReader(bytes).entries().associate { it.name to it.content }
|
||||
}
|
||||
}
|
||||
@ -95,17 +95,19 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
|
||||
val outputDir = createTempDir()
|
||||
every { dirFinder.getCSVOutputDir() } returns outputDir
|
||||
behavior.onExportCSV()
|
||||
taskRunner.await()
|
||||
verify { screen.showSendFileScreen(any()) }
|
||||
val files = outputDir.listFiles()
|
||||
assertEquals(1, files!!.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnExportCSV_fail() {
|
||||
fun testOnExportCSV_fail() = runTest {
|
||||
val mockDir: UserFile = mock()
|
||||
every { mockDir.resolve(any()) } throws RuntimeException("not writable")
|
||||
every { dirFinder.getCSVOutputDir() } returns mockDir
|
||||
behavior.onExportCSV()
|
||||
taskRunner.await()
|
||||
verify { screen.showMessage(ListHabitsBehavior.Message.COULD_NOT_EXPORT) }
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ import dev.mokkery.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.isoron.uhabits.core.BaseUnitTest
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.tasks.CoroutineTaskRunner
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -62,6 +63,7 @@ class ShowHabitMenuPresenterTest : BaseUnitTest() {
|
||||
val outputDir = createTempDir()
|
||||
every { system.getCSVOutputDir() } returns outputDir
|
||||
menu.onExportCSV()
|
||||
(taskRunner as CoroutineTaskRunner).await()
|
||||
val files = outputDir.listFiles()
|
||||
assertEquals(1, files!!.size)
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
package org.isoron.platform
|
||||
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.startCoroutine
|
||||
|
||||
actual fun <T> runSuspend(block: suspend () -> T): T {
|
||||
var result: Result<T>? = null
|
||||
block.startCoroutine(
|
||||
object : Continuation<T> {
|
||||
override val context = EmptyCoroutineContext
|
||||
override fun resumeWith(r: Result<T>) {
|
||||
result = r
|
||||
}
|
||||
}
|
||||
)
|
||||
return result?.getOrThrow()
|
||||
?: throw IllegalStateException(
|
||||
"runSuspend: coroutine did not complete synchronously"
|
||||
)
|
||||
}
|
||||
53
uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/Zip.kt
Normal file
53
uhabits-core/src/jsMain/kotlin/org/isoron/platform/io/Zip.kt
Normal file
@ -0,0 +1,53 @@
|
||||
package org.isoron.platform.io
|
||||
|
||||
import kotlinx.coroutines.await
|
||||
import org.khronos.webgl.Uint8Array
|
||||
import org.khronos.webgl.get
|
||||
import org.khronos.webgl.set
|
||||
import kotlin.js.Promise
|
||||
import kotlin.js.json
|
||||
|
||||
@JsModule("jszip")
|
||||
@JsNonModule
|
||||
external class JSZip {
|
||||
fun loadAsync(data: dynamic): Promise<JSZip>
|
||||
fun file(name: String, data: String): JSZip
|
||||
fun forEach(callback: (String, dynamic) -> Unit)
|
||||
fun generateAsync(options: dynamic): Promise<dynamic>
|
||||
}
|
||||
|
||||
actual class ZipReader actual constructor(bytes: ByteArray) {
|
||||
private val data = bytes
|
||||
|
||||
actual suspend fun entries(): List<ZipEntry> {
|
||||
val uint8 = Uint8Array(data.size)
|
||||
for (i in data.indices) uint8[i] = data[i]
|
||||
val zip = JSZip().loadAsync(uint8).await()
|
||||
val result = mutableListOf<ZipEntry>()
|
||||
val files = mutableListOf<Pair<String, dynamic>>()
|
||||
zip.forEach { path, file ->
|
||||
if (!(file.dir as Boolean)) {
|
||||
files.add(path to file)
|
||||
}
|
||||
}
|
||||
for ((name, file) in files) {
|
||||
val content = (file.async("string") as Promise<String>).await()
|
||||
result.add(ZipEntry(name, content))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
actual class ZipWriter {
|
||||
private val zip = JSZip()
|
||||
|
||||
actual fun addEntry(name: String, content: String) {
|
||||
zip.file(name, content)
|
||||
}
|
||||
|
||||
actual suspend fun toBytes(): ByteArray {
|
||||
val uint8 =
|
||||
(zip.generateAsync(json("type" to "uint8array", "compression" to "DEFLATE")) as Promise<Uint8Array>).await()
|
||||
return ByteArray(uint8.length) { uint8[it] }
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
package org.isoron.platform.io
|
||||
|
||||
actual class ZipReader actual constructor(bytes: ByteArray) {
|
||||
private val data = bytes
|
||||
|
||||
actual fun entries(): List<ZipEntry> {
|
||||
val result = mutableListOf<ZipEntry>()
|
||||
var pos = 0
|
||||
while (pos + 30 <= data.size) {
|
||||
val sig = readInt(pos)
|
||||
if (sig != 0x04034b50) break
|
||||
val compressionMethod = readShort(pos + 8)
|
||||
val compressedSize = readInt(pos + 18)
|
||||
val nameLen = readShort(pos + 26)
|
||||
val extraLen = readShort(pos + 28)
|
||||
val nameStart = pos + 30
|
||||
val name = data.copyOfRange(nameStart, nameStart + nameLen).decodeToString()
|
||||
val dataStart = nameStart + nameLen + extraLen
|
||||
if (compressionMethod != 0) {
|
||||
throw UnsupportedOperationException(
|
||||
"Only STORED ZIP entries are supported, got method $compressionMethod"
|
||||
)
|
||||
}
|
||||
val content = data.copyOfRange(dataStart, dataStart + compressedSize).decodeToString()
|
||||
result.add(ZipEntry(name, content))
|
||||
pos = dataStart + compressedSize
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun readInt(offset: Int): Int =
|
||||
(data[offset].toInt() and 0xFF) or
|
||||
((data[offset + 1].toInt() and 0xFF) shl 8) or
|
||||
((data[offset + 2].toInt() and 0xFF) shl 16) or
|
||||
((data[offset + 3].toInt() and 0xFF) shl 24)
|
||||
|
||||
private fun readShort(offset: Int): Int =
|
||||
(data[offset].toInt() and 0xFF) or
|
||||
((data[offset + 1].toInt() and 0xFF) shl 8)
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
package org.isoron.platform.io
|
||||
|
||||
actual class ZipWriter {
|
||||
private val entries = mutableListOf<Pair<String, ByteArray>>()
|
||||
|
||||
actual fun addEntry(name: String, content: String) {
|
||||
entries.add(name to content.encodeToByteArray())
|
||||
}
|
||||
|
||||
actual suspend fun toBytes(): ByteArray {
|
||||
val buf = ZipBuffer()
|
||||
val offsets = mutableListOf<Int>()
|
||||
|
||||
for ((name, data) in entries) {
|
||||
offsets.add(buf.size)
|
||||
val nameBytes = name.encodeToByteArray()
|
||||
val crc = crc32(data)
|
||||
buf.writeInt(0x04034b50)
|
||||
buf.writeShort(20)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(0)
|
||||
buf.writeInt(crc)
|
||||
buf.writeInt(data.size)
|
||||
buf.writeInt(data.size)
|
||||
buf.writeShort(nameBytes.size)
|
||||
buf.writeShort(0)
|
||||
buf.writeBytes(nameBytes)
|
||||
buf.writeBytes(data)
|
||||
}
|
||||
|
||||
val centralDirOffset = buf.size
|
||||
for (i in entries.indices) {
|
||||
val (name, data) = entries[i]
|
||||
val nameBytes = name.encodeToByteArray()
|
||||
val crc = crc32(data)
|
||||
buf.writeInt(0x02014b50)
|
||||
buf.writeShort(20)
|
||||
buf.writeShort(20)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(0)
|
||||
buf.writeInt(crc)
|
||||
buf.writeInt(data.size)
|
||||
buf.writeInt(data.size)
|
||||
buf.writeShort(nameBytes.size)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(0)
|
||||
buf.writeInt(0)
|
||||
buf.writeInt(offsets[i])
|
||||
buf.writeBytes(nameBytes)
|
||||
}
|
||||
val centralDirSize = buf.size - centralDirOffset
|
||||
|
||||
buf.writeInt(0x06054b50)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(0)
|
||||
buf.writeShort(entries.size)
|
||||
buf.writeShort(entries.size)
|
||||
buf.writeInt(centralDirSize)
|
||||
buf.writeInt(centralDirOffset)
|
||||
buf.writeShort(0)
|
||||
|
||||
return buf.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
private class ZipBuffer {
|
||||
private var data = ByteArray(4096)
|
||||
var size = 0
|
||||
private set
|
||||
|
||||
fun writeShort(v: Int) {
|
||||
ensureCapacity(2)
|
||||
data[size++] = (v and 0xFF).toByte()
|
||||
data[size++] = ((v shr 8) and 0xFF).toByte()
|
||||
}
|
||||
|
||||
fun writeInt(v: Int) {
|
||||
ensureCapacity(4)
|
||||
data[size++] = (v and 0xFF).toByte()
|
||||
data[size++] = ((v shr 8) and 0xFF).toByte()
|
||||
data[size++] = ((v shr 16) and 0xFF).toByte()
|
||||
data[size++] = ((v shr 24) and 0xFF).toByte()
|
||||
}
|
||||
|
||||
fun writeBytes(bytes: ByteArray) {
|
||||
ensureCapacity(bytes.size)
|
||||
bytes.copyInto(data, size)
|
||||
size += bytes.size
|
||||
}
|
||||
|
||||
fun toByteArray(): ByteArray = data.copyOf(size)
|
||||
|
||||
private fun ensureCapacity(needed: Int) {
|
||||
if (size + needed > data.size) {
|
||||
data = data.copyOf(maxOf(data.size * 2, size + needed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val crcTable = IntArray(256).also { table ->
|
||||
for (n in 0..255) {
|
||||
var c = n
|
||||
repeat(8) {
|
||||
c = if (c and 1 != 0) (c ushr 1) xor 0xEDB88320.toInt() else c ushr 1
|
||||
}
|
||||
table[n] = c
|
||||
}
|
||||
}
|
||||
|
||||
private fun crc32(data: ByteArray): Int {
|
||||
var crc = -1
|
||||
for (b in data) {
|
||||
crc = (crc ushr 8) xor crcTable[(crc xor b.toInt()) and 0xFF]
|
||||
}
|
||||
return crc xor -1
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker 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.
|
||||
*
|
||||
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.platform
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
actual fun <T> runSuspend(block: suspend () -> T): T = runBlocking { block() }
|
||||
@ -20,12 +20,14 @@
|
||||
package org.isoron.platform.io
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
actual class ZipReader actual constructor(bytes: ByteArray) {
|
||||
private val data = bytes
|
||||
|
||||
actual fun entries(): List<ZipEntry> {
|
||||
actual suspend fun entries(): List<ZipEntry> {
|
||||
val result = mutableListOf<ZipEntry>()
|
||||
val zis = ZipInputStream(ByteArrayInputStream(data))
|
||||
var entry = zis.nextEntry
|
||||
@ -38,3 +40,19 @@ actual class ZipReader actual constructor(bytes: ByteArray) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
actual class ZipWriter {
|
||||
private val baos = ByteArrayOutputStream()
|
||||
private val zos = ZipOutputStream(baos)
|
||||
|
||||
actual fun addEntry(name: String, content: String) {
|
||||
zos.putNextEntry(java.util.zip.ZipEntry(name))
|
||||
zos.write(content.toByteArray())
|
||||
zos.closeEntry()
|
||||
}
|
||||
|
||||
actual suspend fun toBytes(): ByteArray {
|
||||
zos.close()
|
||||
return baos.toByteArray()
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker 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.
|
||||
*
|
||||
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.io
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
actual class ZipWriter {
|
||||
private val baos = ByteArrayOutputStream()
|
||||
private val zos = ZipOutputStream(baos)
|
||||
|
||||
actual fun addEntry(name: String, content: String) {
|
||||
zos.putNextEntry(java.util.zip.ZipEntry(name))
|
||||
zos.write(content.toByteArray())
|
||||
zos.closeEntry()
|
||||
}
|
||||
|
||||
actual suspend fun toBytes(): ByteArray {
|
||||
zos.close()
|
||||
return baos.toByteArray()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user