mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Update Ktor, use new progress feature, fix backup creation
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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?>
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user