Update Ktor, use new progress feature, fix backup creation

This commit is contained in:
Syer10
2021-05-28 16:15:03 -04:00
parent 26d8f0145d
commit 6d47cea979
15 changed files with 142 additions and 56 deletions

View File

@@ -46,7 +46,7 @@ dependencies {
kapt("com.github.stephanenicolas.toothpick:toothpick-compiler:3.1.0") kapt("com.github.stephanenicolas.toothpick:toothpick-compiler:3.1.0")
// Http client // Http client
val ktorVersion = "1.5.4" val ktorVersion = "1.6.0"
implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-okhttp:$ktorVersion") implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
implementation("io.ktor:ktor-client-serialization:$ktorVersion") implementation("io.ktor:ktor-client-serialization:$ktorVersion")

View File

@@ -14,13 +14,13 @@ import ca.gosyer.data.server.requests.backupFileExportRequest
import ca.gosyer.data.server.requests.backupFileImportRequest import ca.gosyer.data.server.requests.backupFileImportRequest
import ca.gosyer.data.server.requests.backupImportRequest import ca.gosyer.data.server.requests.backupImportRequest
import ca.gosyer.util.lang.withIOContext import ca.gosyer.util.lang.withIOContext
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.formData import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData import io.ktor.client.request.forms.submitFormWithBinaryData
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.Headers import io.ktor.http.Headers
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
import io.ktor.http.content.MultiPartData
import io.ktor.http.contentType import io.ktor.http.contentType
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@@ -30,7 +30,7 @@ class BackupInteractionHandler @Inject constructor(
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
) : BaseInteractionHandler(client, serverPreferences) { ) : BaseInteractionHandler(client, serverPreferences) {
suspend fun importBackupFile(file: File) = withIOContext { suspend fun importBackupFile(file: File, block: HttpRequestBuilder.() -> Unit = {}) = withIOContext {
client.submitFormWithBinaryData<HttpResponse>( client.submitFormWithBinaryData<HttpResponse>(
serverUrl + backupFileImportRequest(), serverUrl + backupFileImportRequest(),
formData = formData { formData = formData {
@@ -41,28 +41,32 @@ class BackupInteractionHandler @Inject constructor(
append(HttpHeaders.ContentDisposition, "filename=backup.json") append(HttpHeaders.ContentDisposition, "filename=backup.json")
} }
) )
} },
block = block
) )
} }
suspend fun importBackup(backup: Backup) = withIOContext { suspend fun importBackup(backup: Backup, block: HttpRequestBuilder.() -> Unit = {}) = withIOContext {
client.postRepeat<HttpResponse>( client.postRepeat<HttpResponse>(
serverUrl + backupImportRequest() serverUrl + backupImportRequest()
) { ) {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
body = backup body = backup
block()
} }
} }
suspend fun exportBackupFile() = withIOContext { suspend fun exportBackupFile(block: HttpRequestBuilder.() -> Unit = {}) = withIOContext {
client.getRepeat<MultiPartData>( client.getRepeat<HttpResponse>(
serverUrl + backupFileExportRequest() serverUrl + backupFileExportRequest(),
block
) )
} }
suspend fun exportBackup() = withIOContext { suspend fun exportBackup(block: HttpRequestBuilder.() -> Unit = {}) = withIOContext {
client.getRepeat<Backup>( client.getRepeat<Backup>(
serverUrl + backupExportRequest() serverUrl + backupExportRequest(),
block
) )
} }
} }

View File

@@ -87,9 +87,9 @@ open class BaseInteractionHandler(
} }
} }
suspend fun imageFromUrl(client: Http, imageUrl: String): ImageBitmap { suspend fun imageFromUrl(client: Http, imageUrl: String, block: HttpRequestBuilder.() -> Unit): ImageBitmap {
return repeat { return repeat {
ca.gosyer.util.compose.imageFromUrl(client, imageUrl) ca.gosyer.util.compose.imageFromUrl(client, imageUrl, block)
} }
} }
} }

View File

@@ -15,6 +15,7 @@ import ca.gosyer.data.server.requests.getMangaChaptersQuery
import ca.gosyer.data.server.requests.getPageQuery import ca.gosyer.data.server.requests.getPageQuery
import ca.gosyer.data.server.requests.updateChapterRequest import ca.gosyer.data.server.requests.updateChapterRequest
import ca.gosyer.util.lang.withIOContext import ca.gosyer.util.lang.withIOContext
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.parameter import io.ktor.client.request.parameter
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
@@ -113,16 +114,17 @@ class ChapterInteractionHandler @Inject constructor(
markPreviousRead markPreviousRead
) )
suspend fun getPage(mangaId: Long, chapterIndex: Int, pageNum: Int) = withIOContext { suspend fun getPage(mangaId: Long, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = withIOContext {
imageFromUrl( imageFromUrl(
client, client,
serverUrl + getPageQuery(mangaId, chapterIndex, pageNum) serverUrl + getPageQuery(mangaId, chapterIndex, pageNum),
block
) )
} }
suspend fun getPage(chapter: Chapter, pageNum: Int) = getPage(chapter.mangaId, chapter.index, pageNum) suspend fun getPage(chapter: Chapter, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(chapter.mangaId, chapter.index, pageNum, block)
suspend fun getPage(manga: Manga, chapterIndex: Int, pageNum: Int) = getPage(manga.id, chapterIndex, pageNum) suspend fun getPage(manga: Manga, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(manga.id, chapterIndex, pageNum, block)
suspend fun getPage(manga: Manga, chapter: Chapter, pageNum: Int) = getPage(manga.id, chapter.index, pageNum) suspend fun getPage(manga: Manga, chapter: Chapter, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(manga.id, chapter.index, pageNum, block)
} }

View File

@@ -15,6 +15,7 @@ import ca.gosyer.data.server.requests.apkUninstallQuery
import ca.gosyer.data.server.requests.apkUpdateQuery import ca.gosyer.data.server.requests.apkUpdateQuery
import ca.gosyer.data.server.requests.extensionListQuery import ca.gosyer.data.server.requests.extensionListQuery
import ca.gosyer.util.lang.withIOContext import ca.gosyer.util.lang.withIOContext
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import javax.inject.Inject import javax.inject.Inject
@@ -47,10 +48,11 @@ class ExtensionInteractionHandler @Inject constructor(
) )
} }
suspend fun getApkIcon(extension: Extension) = withIOContext { suspend fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit) = withIOContext {
imageFromUrl( imageFromUrl(
client, client,
serverUrl + apkIconQuery(extension.apkName) serverUrl + apkIconQuery(extension.apkName),
block
) )
} }
} }

View File

@@ -12,6 +12,7 @@ import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.requests.mangaQuery import ca.gosyer.data.server.requests.mangaQuery
import ca.gosyer.data.server.requests.mangaThumbnailQuery import ca.gosyer.data.server.requests.mangaThumbnailQuery
import ca.gosyer.util.lang.withIOContext import ca.gosyer.util.lang.withIOContext
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.parameter import io.ktor.client.request.parameter
import javax.inject.Inject import javax.inject.Inject
@@ -34,10 +35,11 @@ class MangaInteractionHandler @Inject constructor(
suspend fun getManga(manga: Manga, refresh: Boolean = false) = getManga(manga.id, refresh) suspend fun getManga(manga: Manga, refresh: Boolean = false) = getManga(manga.id, refresh)
suspend fun getMangaThumbnail(mangaId: Long) = withIOContext { suspend fun getMangaThumbnail(mangaId: Long, block: HttpRequestBuilder.() -> Unit) = withIOContext {
imageFromUrl( imageFromUrl(
client, client,
serverUrl + mangaThumbnailQuery(mangaId) serverUrl + mangaThumbnailQuery(mangaId),
block
) )
} }
} }

View File

@@ -24,10 +24,13 @@ import ca.gosyer.common.di.AppScope
import ca.gosyer.data.server.Http import ca.gosyer.data.server.Http
import ca.gosyer.util.compose.imageFromUrl import ca.gosyer.util.compose.imageFromUrl
import ca.gosyer.util.lang.throwIfCancellation import ca.gosyer.util.lang.throwIfCancellation
import io.ktor.client.features.onDownload
import io.ktor.client.request.HttpRequestBuilder
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.max
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@Composable @Composable
@@ -46,6 +49,7 @@ fun KtorImage(
BoxWithConstraints { BoxWithConstraints {
val drawable: MutableState<ImageBitmap?> = remember { mutableStateOf(null) } val drawable: MutableState<ImageBitmap?> = remember { mutableStateOf(null) }
val loading: MutableState<Boolean> = remember { mutableStateOf(true) } val loading: MutableState<Boolean> = remember { mutableStateOf(true) }
val progress: MutableState<Float?> = remember { mutableStateOf(null) }
val error: MutableState<String?> = remember { mutableStateOf(null) } val error: MutableState<String?> = remember { mutableStateOf(null) }
DisposableEffect(imageUrl) { DisposableEffect(imageUrl) {
val handler = CoroutineExceptionHandler { _, throwable -> val handler = CoroutineExceptionHandler { _, throwable ->
@@ -54,7 +58,11 @@ fun KtorImage(
} }
val job = GlobalScope.launch(handler) { val job = GlobalScope.launch(handler) {
if (drawable.value == null) { if (drawable.value == null) {
drawable.value = getImage(client, imageUrl, retries) drawable.value = getImage(client, imageUrl, retries) {
onDownload { bytesSentTotal, contentLength ->
progress.value = max(bytesSentTotal.toFloat() / contentLength, 1.0F)
}
}
} }
loading.value = false loading.value = false
} }
@@ -77,17 +85,17 @@ fun KtorImage(
colorFilter = colorFilter colorFilter = colorFilter
) )
} else { } else {
LoadingScreen(loading.value, loadingModifier, error.value) LoadingScreen(loading.value, loadingModifier, progress.value, error.value)
} }
} }
} }
private suspend fun getImage(client: Http, imageUrl: String, retries: Int = 3): ImageBitmap { private suspend fun getImage(client: Http, imageUrl: String, retries: Int = 3, block: HttpRequestBuilder.() -> Unit): ImageBitmap {
var attempt = 1 var attempt = 1
var lastException: Exception var lastException: Exception
do { do {
try { try {
return imageFromUrl(client, imageUrl) return imageFromUrl(client, imageUrl, block)
} catch (e: Exception) { } catch (e: Exception) {
e.throwIfCancellation() e.throwIfCancellation()
lastException = e lastException = e

View File

@@ -21,6 +21,8 @@ import androidx.compose.ui.unit.min
fun LoadingScreen( fun LoadingScreen(
isLoading: Boolean = true, isLoading: Boolean = true,
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
/*@FloatRange(from = 0.0, to = 1.0)*/
progress: Float? = null,
errorMessage: String? = null, errorMessage: String? = null,
retryMessage: String = "Retry", retryMessage: String = "Retry",
retry: (() -> Unit)? = null retry: (() -> Unit)? = null
@@ -31,7 +33,11 @@ fun LoadingScreen(
val size = remember(maxHeight, maxWidth) { val size = remember(maxHeight, maxWidth) {
min(maxHeight, maxWidth) / 2 min(maxHeight, maxWidth) / 2
} }
CircularProgressIndicator(Modifier.align(Alignment.Center).size(size)) if (progress != null) {
CircularProgressIndicator(progress, Modifier.align(Alignment.Center).size(size))
} else {
CircularProgressIndicator(Modifier.align(Alignment.Center).size(size))
}
} else { } else {
ErrorScreen(errorMessage, modifier, retryMessage, retry) ErrorScreen(errorMessage, modifier, retryMessage, retry)
} }

View File

@@ -201,6 +201,7 @@ fun ReaderMenu(chapterIndex: Int, mangaId: Long, setHotkeys: (List<KeyboardShort
fun ReaderImage( fun ReaderImage(
imageIndex: Int, imageIndex: Int,
drawable: ImageBitmap?, drawable: ImageBitmap?,
progress: Float?,
status: ReaderPage.Status, status: ReaderPage.Status,
error: String?, error: String?,
imageModifier: Modifier = Modifier.fillMaxSize(), imageModifier: Modifier = Modifier.fillMaxSize(),
@@ -216,7 +217,7 @@ fun ReaderImage(
contentScale = contentScale contentScale = contentScale
) )
} else { } else {
LoadingScreen(status == ReaderPage.Status.QUEUE, loadingModifier, error) { retry(imageIndex) } LoadingScreen(status == ReaderPage.Status.QUEUE, loadingModifier, progress, error) { retry(imageIndex) }
} }
} }

View File

@@ -13,6 +13,7 @@ import ca.gosyer.ui.reader.model.ReaderPage
import ca.gosyer.util.lang.throwIfCancellation import ca.gosyer.util.lang.throwIfCancellation
import ca.gosyer.util.system.CKLogger import ca.gosyer.util.system.CKLogger
import io.github.kerubistan.kroki.coroutines.priorityChannel import io.github.kerubistan.kroki.coroutines.priorityChannel
import io.ktor.client.features.onDownload
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
@@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.max
class TachideskPageLoader( class TachideskPageLoader(
context: CoroutineContext, context: CoroutineContext,
@@ -58,7 +60,11 @@ class TachideskPageLoader(
debug { "Loading page ${page.index}" } debug { "Loading page ${page.index}" }
if (page.status.value == ReaderPage.Status.QUEUE) { if (page.status.value == ReaderPage.Status.QUEUE) {
try { try {
page.bitmap.value = chapterHandler.getPage(chapter.chapter, page.index) page.bitmap.value = chapterHandler.getPage(chapter.chapter, page.index) {
onDownload { bytesSentTotal, contentLength ->
page.progress.value = max(bytesSentTotal.toFloat() / contentLength, 1.0F)
}
}
page.status.value = ReaderPage.Status.READY page.status.value = ReaderPage.Status.READY
page.error.value = null page.error.value = null
} catch (e: Exception) { } catch (e: Exception) {
@@ -107,6 +113,7 @@ class TachideskPageLoader(
ReaderPage( ReaderPage(
it, it,
MutableStateFlow(null), MutableStateFlow(null),
MutableStateFlow(null),
MutableStateFlow(ReaderPage.Status.QUEUE), MutableStateFlow(ReaderPage.Status.QUEUE),
MutableStateFlow(null) MutableStateFlow(null)
) )

View File

@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
data class ReaderPage( data class ReaderPage(
val index: Int, val index: Int,
val bitmap: MutableStateFlow<ImageBitmap?>, val bitmap: MutableStateFlow<ImageBitmap?>,
val progress: MutableStateFlow<Float?>,
val status: MutableStateFlow<Status>, val status: MutableStateFlow<Status>,
val error: MutableStateFlow<String?> val error: MutableStateFlow<String?>
) { ) {

View File

@@ -63,6 +63,7 @@ fun ContinuousReader(
ReaderImage( ReaderImage(
image.index, image.index,
image.bitmap.collectAsState().value, image.bitmap.collectAsState().value,
image.progress.collectAsState().value,
image.status.collectAsState().value, image.status.collectAsState().value,
image.error.collectAsState().value, image.error.collectAsState().value,
loadingModifier = pageModifier, loadingModifier = pageModifier,

View File

@@ -101,6 +101,7 @@ fun HandlePager(
ReaderImage( ReaderImage(
image.index, image.index,
image.bitmap.collectAsState().value, image.bitmap.collectAsState().value,
image.progress.collectAsState().value,
image.status.collectAsState().value, image.status.collectAsState().value,
image.error.collectAsState().value, image.error.collectAsState().value,
loadingModifier = pageModifier, loadingModifier = pageModifier,

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -32,39 +33,53 @@ import ca.gosyer.util.system.CKLogger
import ca.gosyer.util.system.filePicker import ca.gosyer.util.system.filePicker
import ca.gosyer.util.system.fileSaver import ca.gosyer.util.system.fileSaver
import com.github.zsoltk.compose.router.BackStack import com.github.zsoltk.compose.router.BackStack
import io.ktor.client.features.onDownload
import io.ktor.client.features.onUpload
import io.ktor.utils.io.jvm.javaio.copyTo
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max
class SettingsBackupViewModel @Inject constructor( class SettingsBackupViewModel @Inject constructor(
private val backupHandler: BackupInteractionHandler private val backupHandler: BackupInteractionHandler
) : ViewModel() { ) : ViewModel() {
private val _restoring = MutableStateFlow(false) private val _restoring = MutableStateFlow(false)
val restoring = _restoring.asStateFlow() val restoring = _restoring.asStateFlow()
private val _restoreError = MutableStateFlow(false) private val _restoringProgress = MutableStateFlow<Float?>(null)
val restoreError = _restoreError.asStateFlow() val restoringProgress = _restoringProgress.asStateFlow()
private val _restoreStatus = MutableStateFlow<Status>(Status.Nothing)
internal val restoreStatus = _restoreStatus.asStateFlow()
private val _creating = MutableStateFlow(false) private val _creating = MutableStateFlow(false)
val creating = _creating.asStateFlow() val creating = _creating.asStateFlow()
private val _creatingError = MutableStateFlow(false) private val _creatingProgress = MutableStateFlow<Float?>(null)
val creatingError = _creatingError.asStateFlow() val creatingProgress = _creatingProgress.asStateFlow()
private val _creatingStatus = MutableStateFlow<Status>(Status.Nothing)
internal val creatingStatus = _creatingStatus.asStateFlow()
fun restoreFile(file: File?) { fun restoreFile(file: File?) {
scope.launch { scope.launch {
if (file == null || !file.exists()) { if (file == null || !file.exists()) {
info { "Invalid file ${file?.absolutePath}" } info { "Invalid file ${file?.absolutePath}" }
} else { } else {
_restoreError.value = false _restoreStatus.value = Status.Nothing
_restoringProgress.value = null
_restoring.value = true _restoring.value = true
try { try {
backupHandler.importBackupFile(file) backupHandler.importBackupFile(file) {
onUpload { bytesSentTotal, contentLength ->
_restoringProgress.value = max(bytesSentTotal.toFloat() / contentLength, 1.0F)
}
}
} catch (e: Exception) { } catch (e: Exception) {
info(e) { "Error importing backup" } info(e) { "Error importing backup" }
_restoreError.value = true _restoreStatus.value = Status.Error
} finally { } finally {
_restoring.value = false _restoring.value = false
_restoreStatus.value = Status.Success
} }
} }
} }
@@ -76,20 +91,36 @@ class SettingsBackupViewModel @Inject constructor(
info { "Invalid file ${file?.absolutePath}" } info { "Invalid file ${file?.absolutePath}" }
} else { } else {
if (file.exists()) file.delete() if (file.exists()) file.delete()
_creatingError.value = false _creatingStatus.value = Status.Nothing
_creatingProgress.value = null
_creating.value = true _creating.value = true
try { try {
val backup = backupHandler.exportBackupFile() val backup = backupHandler.exportBackupFile {
onDownload { bytesSentTotal, contentLength ->
_creatingProgress.value = max(bytesSentTotal.toFloat() / contentLength, 0.99F)
}
}
file.outputStream().use {
backup.content.copyTo(it)
}
} catch (e: Exception) { } catch (e: Exception) {
info(e) { "Error exporting backup" } info(e) { "Error exporting backup" }
_creatingError.value = true _creatingStatus.value = Status.Error
} finally { } finally {
_creatingProgress.value = 1.0F
_creating.value = false _creating.value = false
_creatingStatus.value = Status.Success
} }
} }
} }
} }
internal sealed class Status {
object Nothing : Status()
object Success : Status()
object Error : Status()
}
private companion object : CKLogger({}) private companion object : CKLogger({})
} }
@@ -97,9 +128,11 @@ class SettingsBackupViewModel @Inject constructor(
fun SettingsBackupScreen(navController: BackStack<Route>) { fun SettingsBackupScreen(navController: BackStack<Route>) {
val vm = viewModel<SettingsBackupViewModel>() val vm = viewModel<SettingsBackupViewModel>()
val restoring by vm.restoring.collectAsState() val restoring by vm.restoring.collectAsState()
val restoreError by vm.restoreError.collectAsState() val restoringProgress by vm.restoringProgress.collectAsState()
val restoreStatus by vm.restoreStatus.collectAsState()
val creating by vm.creating.collectAsState() val creating by vm.creating.collectAsState()
val creatingError by vm.creatingError.collectAsState() val creatingProgress by vm.creatingProgress.collectAsState()
val creatingStatus by vm.creatingStatus.collectAsState()
Column { Column {
Toolbar("Backup Settings", navController, true) Toolbar("Backup Settings", navController, true)
LazyColumn { LazyColumn {
@@ -108,7 +141,8 @@ fun SettingsBackupScreen(navController: BackStack<Route>) {
"Restore Backup", "Restore Backup",
"Restore a backup into Tachidesk", "Restore a backup into Tachidesk",
restoring, restoring,
restoreError restoringProgress,
restoreStatus
) { ) {
filePicker("json") { filePicker("json") {
vm.restoreFile(it.selectedFile) vm.restoreFile(it.selectedFile)
@@ -118,9 +152,10 @@ fun SettingsBackupScreen(navController: BackStack<Route>) {
"Create Backup", "Create Backup",
"Create a backup from Tachidesk", "Create a backup from Tachidesk",
creating, creating,
creatingError creatingProgress,
creatingStatus
) { ) {
fileSaver("test.json", "json") { fileSaver("backup.json", "json") {
vm.createFile(it.selectedFile) vm.createFile(it.selectedFile)
} }
} }
@@ -130,7 +165,7 @@ fun SettingsBackupScreen(navController: BackStack<Route>) {
} }
@Composable @Composable
fun PreferenceFile(title: String, subtitle: String, working: Boolean, error: Boolean, onClick: () -> Unit) { private fun PreferenceFile(title: String, subtitle: String, working: Boolean, progress: Float?, status: SettingsBackupViewModel.Status, onClick: () -> Unit) {
PreferenceRow( PreferenceRow(
title = title, title = title,
onClick = onClick, onClick = onClick,
@@ -144,16 +179,31 @@ fun PreferenceFile(title: String, subtitle: String, working: Boolean, error: Boo
val modifier = Modifier.align(Alignment.Center) val modifier = Modifier.align(Alignment.Center)
.size(size) .size(size)
if (working) { if (working) {
CircularProgressIndicator( if (progress != null) {
modifier CircularProgressIndicator(
) progress,
} else if (error) { modifier
Icon( )
Icons.Default.Warning, } else {
contentDescription = null, CircularProgressIndicator(
modifier = modifier, modifier
tint = Color.Red )
) }
} else if (status != SettingsBackupViewModel.Status.Nothing) {
when (status) {
SettingsBackupViewModel.Status.Error -> Icon(
Icons.Default.Warning,
contentDescription = null,
modifier = modifier,
tint = Color.Red
)
SettingsBackupViewModel.Status.Success -> Icon(
Icons.Default.Check,
contentDescription = null,
modifier = modifier
)
else -> Unit
}
} }
} }
} }

View File

@@ -9,6 +9,7 @@ package ca.gosyer.util.compose
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import ca.gosyer.data.server.Http import ca.gosyer.data.server.Http
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readBytes import io.ktor.client.statement.readBytes
@@ -19,6 +20,6 @@ fun imageFromFile(file: File): ImageBitmap {
return Image.makeFromEncoded(file.readBytes()).asImageBitmap() return Image.makeFromEncoded(file.readBytes()).asImageBitmap()
} }
suspend fun imageFromUrl(client: Http, url: String): ImageBitmap { suspend fun imageFromUrl(client: Http, url: String, block: HttpRequestBuilder.() -> Unit): ImageBitmap {
return Image.makeFromEncoded(client.get<HttpResponse>(url).readBytes()).asImageBitmap() return Image.makeFromEncoded(client.get<HttpResponse>(url, block).readBytes()).asImageBitmap()
} }