diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 8aaa253a..0cd695fb 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -135,7 +135,6 @@ android { "META-INF/README.md", "META-INF/NOTICE", "META-INF/*.kotlin_module", - "META-INF/*.version", )) } } diff --git a/build.gradle.kts b/build.gradle.kts index f7d60344..6b204188 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -65,11 +65,11 @@ subprojects { } tasks.withType { source(files("src")) - exclude("ca/gosyer/jui/*/build") + exclude("ca/gosyer/jui/*/build", "graphql") } tasks.withType { source(files("src")) - exclude("ca/gosyer/jui/*/build") + exclude("ca/gosyer/jui/*/build", "ca/gosyer/jui/*/build") } plugins.withType { configure { diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/io/OkioExtensions.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/io/OkioExtensions.kt index e9014b94..08996d05 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/io/OkioExtensions.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/io/OkioExtensions.kt @@ -7,6 +7,7 @@ package ca.gosyer.jui.core.io import ca.gosyer.jui.core.lang.withIOContext +import io.ktor.utils.io.ByteReadChannel import okio.Buffer import okio.BufferedSink import okio.BufferedSource @@ -15,6 +16,8 @@ import okio.Path import okio.Source import okio.buffer import okio.use +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext suspend fun Source.saveTo(path: Path) { withIOContext { @@ -36,3 +39,5 @@ suspend fun Source.copyTo(sink: BufferedSink) { } fun ByteArray.source(): BufferedSource = Buffer().write(this) + +expect suspend fun ByteReadChannel.toSource(context: CoroutineContext = EmptyCoroutineContext): Source diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/IosOkio.kt b/core/src/iosMain/kotlin/ca/gosyer/jui/core/io/IosOkioExtensions.kt similarity index 93% rename from presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/IosOkio.kt rename to core/src/iosMain/kotlin/ca/gosyer/jui/core/io/IosOkioExtensions.kt index 23a0a4c6..242d0830 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/IosOkio.kt +++ b/core/src/iosMain/kotlin/ca/gosyer/jui/core/io/IosOkioExtensions.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.jui.ui.util.lang +package ca.gosyer.jui.core.io import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.cancel @@ -17,7 +17,7 @@ import kotlin.coroutines.CoroutineContext actual suspend fun ByteReadChannel.toSource(context: CoroutineContext): Source { val channel = this - return object : okio.Source { + return object : Source { override fun close() { channel.cancel() } diff --git a/core/src/jvmMain/kotlin/ca/gosyer/jui/core/io/JvmOkioExtensions.kt b/core/src/jvmMain/kotlin/ca/gosyer/jui/core/io/JvmOkioExtensions.kt new file mode 100644 index 00000000..94e23359 --- /dev/null +++ b/core/src/jvmMain/kotlin/ca/gosyer/jui/core/io/JvmOkioExtensions.kt @@ -0,0 +1,10 @@ +package ca.gosyer.jui.core.io + +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.jvm.javaio.toInputStream +import kotlinx.coroutines.Job +import okio.Source +import okio.source +import kotlin.coroutines.CoroutineContext + +actual suspend fun ByteReadChannel.toSource(context: CoroutineContext): Source = toInputStream(context[Job]).source() diff --git a/data/src/commonMain/graphql/Backup.graphql b/data/src/commonMain/graphql/Backup.graphql new file mode 100644 index 00000000..edb30a74 --- /dev/null +++ b/data/src/commonMain/graphql/Backup.graphql @@ -0,0 +1,35 @@ +fragment RestoreStatusFragment on BackupRestoreStatus { + state + mangaProgress + totalManga +} + +query ValidateBackup($backup: Upload!) { + validateBackup(input: {backup: $backup}) { + missingSources { + id + name + } + } +} + +mutation RestoreBackup($backup: Upload!) { + restoreBackup(input: {backup: $backup}) { + id + status { + ...RestoreStatusFragment + } + } +} + +query RestoreStatus($id: String!) { + restoreStatus(id: $id) { + ...RestoreStatusFragment + } +} + +mutation CreateBackup($includeCategories: Boolean!, $includeChapters: Boolean!) { + createBackup(input: {includeCategories: $includeCategories, includeChapters: $includeChapters}) { + url + } +} diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt index 184c6a76..5bad9d47 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt @@ -7,8 +7,10 @@ package ca.gosyer.jui.data import ca.gosyer.jui.core.lang.addSuffix +import ca.gosyer.jui.data.backup.BackupRepositoryImpl import ca.gosyer.jui.data.chapter.ChapterRepositoryImpl import ca.gosyer.jui.data.settings.SettingsRepositoryImpl +import ca.gosyer.jui.domain.backup.service.BackupRepository import ca.gosyer.jui.domain.chapter.service.ChapterRepository import ca.gosyer.jui.domain.server.Http import ca.gosyer.jui.domain.server.service.ServerPreferences @@ -59,4 +61,11 @@ interface DataComponent : SharedDataComponent { http: Http, serverPreferences: ServerPreferences, ): ChapterRepository = ChapterRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + + @Provides + fun backupRepository( + apolloClient: ApolloClient, + http: Http, + serverPreferences: ServerPreferences, + ): BackupRepository = BackupRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) } diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/backup/BackupRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/backup/BackupRepositoryImpl.kt new file mode 100644 index 00000000..c4753495 --- /dev/null +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/backup/BackupRepositoryImpl.kt @@ -0,0 +1,120 @@ +package ca.gosyer.jui.data.backup + +import ca.gosyer.jui.core.io.toSource +import ca.gosyer.jui.data.graphql.CreateBackupMutation +import ca.gosyer.jui.data.graphql.RestoreBackupMutation +import ca.gosyer.jui.data.graphql.RestoreStatusQuery +import ca.gosyer.jui.data.graphql.ValidateBackupQuery +import ca.gosyer.jui.data.graphql.fragment.RestoreStatusFragment +import ca.gosyer.jui.data.graphql.type.BackupRestoreState +import ca.gosyer.jui.domain.backup.model.BackupValidationResult +import ca.gosyer.jui.domain.backup.model.RestoreState +import ca.gosyer.jui.domain.backup.model.RestoreStatus +import ca.gosyer.jui.domain.backup.service.BackupRepository +import ca.gosyer.jui.domain.server.Http +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.DefaultUpload +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.Url +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import okio.Source +import okio.buffer + +class BackupRepositoryImpl( + private val apolloClient: ApolloClient, + private val http: Http, + private val serverUrl: Url, +) : BackupRepository { + + override fun validateBackup(source: Source): Flow { + return apolloClient.query( + ValidateBackupQuery( + DefaultUpload.Builder() + .content { + it.writeAll(source.buffer()) + } + .fileName("backup.tachibk") + .contentType("application/octet-stream") + .build() + ) + ).toFlow() + .map { + BackupValidationResult( + missingSources = it.dataAssertNoErrors.validateBackup.missingSources.map { source -> + "${source.name} (${source.id})" + }, + missingTrackers = emptyList() + ) + } + } + + override fun restoreBackup(source: Source): Flow> { + return apolloClient.mutation( + RestoreBackupMutation( + DefaultUpload.Builder() + .content { + it.writeAll(source.buffer()) + } + .fileName("backup.tachibk") + .contentType("application/octet-stream") + .build() + ) + ).toFlow() + .map { + val data = it.dataAssertNoErrors + data.restoreBackup.id to data.restoreBackup.status!! + .restoreStatusFragment + .toRestoreStatus() + } + } + + override fun restoreStatus(id: String): Flow { + return apolloClient.query( + RestoreStatusQuery(id) + ).toFlow() + .map { + val data = it.dataAssertNoErrors + data.restoreStatus!!.restoreStatusFragment.toRestoreStatus() + } + } + + override fun createBackup( + includeCategories: Boolean, + includeChapters: Boolean, + block: HttpRequestBuilder.() -> Unit, + ): Flow> { + return apolloClient + .mutation( + CreateBackupMutation(includeCategories, includeChapters) + ) + .toFlow() + .map { + val url = it.dataAssertNoErrors.createBackup.url + val response = http.get( + Url("$serverUrl${url}") + ) + val fileName = response.headers["content-disposition"]!! + .substringAfter("filename=") + .trim('"') + fileName to response.bodyAsChannel().toSource() + } + } + + companion object { + private fun RestoreStatusFragment.toRestoreStatus() = RestoreStatus( + when (state) { + BackupRestoreState.IDLE -> RestoreState.IDLE + BackupRestoreState.SUCCESS -> RestoreState.SUCCESS + BackupRestoreState.FAILURE -> RestoreState.FAILURE + BackupRestoreState.RESTORING_CATEGORIES -> RestoreState.RESTORING_CATEGORIES + BackupRestoreState.RESTORING_MANGA -> RestoreState.RESTORING_MANGA + BackupRestoreState.UNKNOWN__ -> RestoreState.UNKNOWN + }, + mangaProgress, + totalManga, + ) + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt index 68dc9bf2..548cfab1 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt @@ -6,7 +6,7 @@ package ca.gosyer.jui.domain.backup.interactor -import ca.gosyer.jui.domain.backup.service.BackupRepositoryOld +import ca.gosyer.jui.domain.backup.service.BackupRepository import io.ktor.client.request.HttpRequestBuilder import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.singleOrNull @@ -16,19 +16,25 @@ import org.lighthousegames.logging.logging class ExportBackupFile @Inject constructor( - private val backupRepositoryOld: BackupRepositoryOld, + private val backupRepository: BackupRepository, ) { suspend fun await( + includeCategories: Boolean, + includeChapters: Boolean, block: HttpRequestBuilder.() -> Unit = {}, onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(block) + ) = asFlow(includeCategories, includeChapters, block) .catch { onError(it) log.warn(it) { "Failed to export backup" } } .singleOrNull() - fun asFlow(block: HttpRequestBuilder.() -> Unit = {}) = backupRepositoryOld.exportBackupFile(block) + fun asFlow( + includeCategories: Boolean, + includeChapters: Boolean, + block: HttpRequestBuilder.() -> Unit = {}, + ) = backupRepository.createBackup(includeCategories, includeChapters, block) companion object { private val log = logging() diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt index 64982282..19b6dfa0 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt @@ -6,24 +6,24 @@ package ca.gosyer.jui.domain.backup.interactor -import ca.gosyer.jui.domain.backup.service.BackupRepositoryOld -import io.ktor.client.request.HttpRequestBuilder +import ca.gosyer.jui.domain.backup.service.BackupRepository import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject +import okio.FileSystem import okio.Path +import okio.SYSTEM import org.lighthousegames.logging.logging class ImportBackupFile @Inject constructor( - private val backupRepositoryOld: BackupRepositoryOld, + private val backupRepository: BackupRepository, ) { suspend fun await( file: Path, - block: HttpRequestBuilder.() -> Unit = {}, onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(file, block) + ) = asFlow(file) .catch { onError(it) log.warn(it) { "Failed to import backup ${file.name}" } @@ -32,8 +32,7 @@ class ImportBackupFile fun asFlow( file: Path, - block: HttpRequestBuilder.() -> Unit = {}, - ) = backupRepositoryOld.importBackupFile(BackupRepositoryOld.buildBackupFormData(file), block) + ) = backupRepository.restoreBackup(FileSystem.SYSTEM.source(file)) companion object { private val log = logging() diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt index e5eac143..cddc08e3 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt @@ -6,24 +6,24 @@ package ca.gosyer.jui.domain.backup.interactor -import ca.gosyer.jui.domain.backup.service.BackupRepositoryOld -import io.ktor.client.request.HttpRequestBuilder +import ca.gosyer.jui.domain.backup.service.BackupRepository import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject +import okio.FileSystem import okio.Path +import okio.SYSTEM import org.lighthousegames.logging.logging class ValidateBackupFile @Inject constructor( - private val backupRepositoryOld: BackupRepositoryOld, + private val backupRepository: BackupRepository, ) { suspend fun await( file: Path, - block: HttpRequestBuilder.() -> Unit = {}, onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(file, block) + ) = asFlow(file) .catch { onError(it) log.warn(it) { "Failed to validate backup ${file.name}" } @@ -32,8 +32,7 @@ class ValidateBackupFile fun asFlow( file: Path, - block: HttpRequestBuilder.() -> Unit = {}, - ) = backupRepositoryOld.validateBackupFile(BackupRepositoryOld.buildBackupFormData(file), block) + ) = backupRepository.validateBackup(FileSystem.SYSTEM.source(file)) companion object { private val log = logging() diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/model/RestoreStatus.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/model/RestoreStatus.kt new file mode 100644 index 00000000..7590627e --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/model/RestoreStatus.kt @@ -0,0 +1,25 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.backup.model + +import kotlinx.serialization.Serializable + +enum class RestoreState { + IDLE, + SUCCESS, + FAILURE, + RESTORING_CATEGORIES, + RESTORING_MANGA, + UNKNOWN, +} + +@Serializable +data class RestoreStatus( + val state: RestoreState, + val completed: Int, + val total: Int, +) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/service/BackupRepository.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/service/BackupRepository.kt new file mode 100644 index 00000000..087d7107 --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/service/BackupRepository.kt @@ -0,0 +1,18 @@ +package ca.gosyer.jui.domain.backup.service + +import ca.gosyer.jui.domain.backup.model.BackupValidationResult +import ca.gosyer.jui.domain.backup.model.RestoreStatus +import io.ktor.client.request.HttpRequestBuilder +import kotlinx.coroutines.flow.Flow +import okio.Source + +interface BackupRepository { + fun validateBackup(source: Source): Flow + fun restoreBackup(source: Source): Flow> + fun restoreStatus(id: String): Flow + fun createBackup( + includeCategories: Boolean, + includeChapters: Boolean, + block: HttpRequestBuilder.() -> Unit, + ): Flow> +} diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/file/AndroidFileChooser.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/file/AndroidFileChooser.kt index 12d182b3..2551eb5f 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/file/AndroidFileChooser.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/file/AndroidFileChooser.kt @@ -18,19 +18,21 @@ import okio.Source import okio.source actual class FileChooser( - private val resultLauncher: ManagedActivityResultLauncher, + private val resultLauncher: ManagedActivityResultLauncher, Uri?>, ) { - actual fun launch(extension: String) { - val mime = MimeTypeMap.getSingleton() - .getMimeTypeFromExtension(extension) ?: return - resultLauncher.launch(mime) + actual fun launch(vararg extensions: String) { + val mimes = extensions.mapNotNull { extension -> + MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(extension) + }.toTypedArray() + resultLauncher.launch(mimes) } } @Composable actual fun rememberFileChooser(onFileFound: (Source) -> Unit): FileChooser { val context = LocalContext.current - val result = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { + val result = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { if (it != null) { context.contentResolver.openInputStream(it)?.source()?.let(onFileFound) } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/file/FileChooser.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/file/FileChooser.kt index 75e9b16f..c0fc0244 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/file/FileChooser.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/file/FileChooser.kt @@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable import okio.Source expect class FileChooser { - fun launch(extension: String) + fun launch(vararg extensions: String) } @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt index 440f3bf5..c46f27cc 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt @@ -45,6 +45,8 @@ import ca.gosyer.jui.core.lang.throwIfCancellation import ca.gosyer.jui.domain.backup.interactor.ExportBackupFile import ca.gosyer.jui.domain.backup.interactor.ImportBackupFile import ca.gosyer.jui.domain.backup.interactor.ValidateBackupFile +import ca.gosyer.jui.domain.backup.model.RestoreState +import ca.gosyer.jui.domain.backup.model.RestoreStatus import ca.gosyer.jui.i18n.MR import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties import ca.gosyer.jui.ui.base.file.rememberFileChooser @@ -53,7 +55,6 @@ import ca.gosyer.jui.ui.base.model.StableHolder import ca.gosyer.jui.ui.base.navigation.Toolbar import ca.gosyer.jui.ui.base.prefs.PreferenceRow import ca.gosyer.jui.ui.main.components.bottomNav -import ca.gosyer.jui.ui.util.lang.toSource import ca.gosyer.jui.ui.viewModel import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter @@ -70,8 +71,6 @@ import com.vanpra.composematerialdialogs.listItems import com.vanpra.composematerialdialogs.rememberMaterialDialogState import com.vanpra.composematerialdialogs.title import io.ktor.client.plugins.onDownload -import io.ktor.client.plugins.onUpload -import io.ktor.client.statement.bodyAsChannel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -172,19 +171,12 @@ class SettingsBackupViewModel fun restoreBackup(file: Path) { importBackupFile - .asFlow(file) { - onUpload { bytesSentTotal, contentLength -> - _restoreStatus.value = Status.InProgress( - (bytesSentTotal.toFloat() / contentLength) - .coerceAtMost(1.0F), - ) - } - } + .asFlow(file) .onStart { _restoreStatus.value = Status.InProgress(null) } .onEach { - _restoreStatus.value = Status.Success + _restoreStatus.value = it.second.toStatus() } .catch { toast(it.message.orEmpty()) @@ -194,6 +186,15 @@ class SettingsBackupViewModel .launchIn(scope) } + private fun RestoreStatus.toStatus() = when (state) { + RestoreState.IDLE -> Status.Success + RestoreState.SUCCESS -> Status.Success + RestoreState.FAILURE -> Status.Error + RestoreState.RESTORING_CATEGORIES -> Status.InProgress(0.01f) + RestoreState.RESTORING_MANGA -> Status.InProgress((completed.toFloat() / total).coerceIn(0f, 1f)) + RestoreState.UNKNOWN -> Status.Error + } + fun stopRestore() { _restoreStatus.value = Status.Error } @@ -203,7 +204,9 @@ class SettingsBackupViewModel fun exportBackup() { exportBackupFile - .asFlow { + .asFlow( + true, true // todo + ) { onDownload { bytesSentTotal, contentLength -> _creatingStatus.value = Status.InProgress( (bytesSentTotal.toFloat() / contentLength) @@ -214,15 +217,12 @@ class SettingsBackupViewModel .onStart { _creatingStatus.value = Status.InProgress(null) } - .onEach { backup -> - val filename = - backup.headers["content-disposition"]?.substringAfter("filename=") - ?.trim('"') ?: "backup" + .onEach { (filename, source) -> tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also { mutex.tryLock() scope.launch { try { - backup.bodyAsChannel().toSource().saveTo(it) + source.saveTo(it) } catch (e: Exception) { e.throwIfCancellation() log.warn(e) { "Error creating backup" } @@ -338,7 +338,7 @@ private fun SettingsBackupScreenContent( stringResource(MR.strings.backup_restore_sub), restoreStatus, ) { - fileChooser.launch("gz") + fileChooser.launch("gz", "tachibk") } PreferenceFile( stringResource(MR.strings.backup_create), diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/Okio.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/Okio.kt deleted file mode 100644 index 1c4b0d24..00000000 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/Okio.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package ca.gosyer.jui.ui.util.lang - -import io.ktor.utils.io.ByteReadChannel -import okio.Source -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext - -expect suspend fun ByteReadChannel.toSource(context: CoroutineContext = EmptyCoroutineContext): Source diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/file/DesktopFileChooser.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/file/DesktopFileChooser.kt index 36c3480c..ad6e1587 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/file/DesktopFileChooser.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/file/DesktopFileChooser.kt @@ -26,9 +26,9 @@ actual class FileChooser( details?.actionPerformed(null) } - actual fun launch(extension: String) { + actual fun launch(vararg extensions: String) { scope.launchDefault { - fileChooser.fileFilter = FileNameExtensionFilter("$extension file", extension) + fileChooser.fileFilter = FileNameExtensionFilter("${extensions.joinToString()} files", *extensions) when (fileChooser.showOpenDialog(null)) { JFileChooser.APPROVE_OPTION -> onFileFound(fileChooser.selectedFile.source()) } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/JvmOkio.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/JvmOkio.kt deleted file mode 100644 index 72126954..00000000 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/JvmOkio.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package ca.gosyer.jui.ui.util.lang - -import io.ktor.utils.io.ByteReadChannel -import io.ktor.utils.io.jvm.javaio.toInputStream -import okio.Source -import okio.source -import kotlin.coroutines.CoroutineContext - -actual suspend fun ByteReadChannel.toSource(context: CoroutineContext): Source = toInputStream().source()