mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Backups
Restmore may be broken
This commit is contained in:
@@ -135,7 +135,6 @@ android {
|
||||
"META-INF/README.md",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/*.kotlin_module",
|
||||
"META-INF/*.version",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +65,11 @@ subprojects {
|
||||
}
|
||||
tasks.withType<org.jmailen.gradle.kotlinter.tasks.LintTask> {
|
||||
source(files("src"))
|
||||
exclude("ca/gosyer/jui/*/build")
|
||||
exclude("ca/gosyer/jui/*/build", "graphql")
|
||||
}
|
||||
tasks.withType<org.jmailen.gradle.kotlinter.tasks.FormatTask> {
|
||||
source(files("src"))
|
||||
exclude("ca/gosyer/jui/*/build")
|
||||
exclude("ca/gosyer/jui/*/build", "ca/gosyer/jui/*/build")
|
||||
}
|
||||
plugins.withType<com.android.build.gradle.BasePlugin> {
|
||||
configure<com.android.build.gradle.BaseExtension> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
35
data/src/commonMain/graphql/Backup.graphql
Normal file
35
data/src/commonMain/graphql/Backup.graphql
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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<BackupValidationResult> {
|
||||
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<Pair<String, RestoreStatus>> {
|
||||
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<RestoreStatus> {
|
||||
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<Pair<String, Source>> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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<BackupValidationResult>
|
||||
fun restoreBackup(source: Source): Flow<Pair<String, RestoreStatus>>
|
||||
fun restoreStatus(id: String): Flow<RestoreStatus>
|
||||
fun createBackup(
|
||||
includeCategories: Boolean,
|
||||
includeChapters: Boolean,
|
||||
block: HttpRequestBuilder.() -> Unit,
|
||||
): Flow<Pair<String, Source>>
|
||||
}
|
||||
@@ -18,19 +18,21 @@ import okio.Source
|
||||
import okio.source
|
||||
|
||||
actual class FileChooser(
|
||||
private val resultLauncher: ManagedActivityResultLauncher<String, Uri?>,
|
||||
private val resultLauncher: ManagedActivityResultLauncher<Array<String>, 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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user