OCR: progress indicator for download
This commit is contained in:
@@ -296,6 +296,7 @@ class MainActivity : ComponentActivity() {
|
||||
onExportFormatChanged = { format -> settingsViewModel.setExportFormat(format) },
|
||||
onExportQualityChanged = { quality -> settingsViewModel.setExportQuality(quality) },
|
||||
onInstallOcrLanguage = settingsViewModel::installLanguage,
|
||||
onCancelOcrDownload = settingsViewModel::cancelOcrDownload,
|
||||
onEnableOcrLanguage = settingsViewModel::setOcrLanguageEnabled,
|
||||
onDeleteUnusedOcrLanguages = settingsViewModel::deleteUnusedOcrLanguages,
|
||||
onBack = nav.back,
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -81,7 +82,12 @@ class OcrLanguageRepository(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun downloadLanguage(code: String) =
|
||||
data class DownloadProgress(
|
||||
val downloadedBytes: Long,
|
||||
val totalBytes: Long?,
|
||||
)
|
||||
|
||||
suspend fun downloadLanguage(code: String, onProgress: (DownloadProgress) -> Unit) =
|
||||
withContext(Dispatchers.IO) {
|
||||
require(code in AVAILABLE_LANGUAGE_CODES)
|
||||
tessdataDir.mkdirs()
|
||||
@@ -102,7 +108,21 @@ class OcrLanguageRepository(
|
||||
}
|
||||
connection.inputStream.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
val totalBytes = connection.contentLengthLong.takeIf { it > 0 }
|
||||
val buffer = ByteArray(8192)
|
||||
var downloadedBytes = 0L
|
||||
input.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
while (true) {
|
||||
coroutineContext.ensureActive()
|
||||
val read = input.read(buffer)
|
||||
if (read < 0) break
|
||||
output.write(buffer, 0, read)
|
||||
downloadedBytes += read
|
||||
onProgress(DownloadProgress(downloadedBytes, totalBytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tempFile.length() < 300_000) {
|
||||
|
||||
@@ -14,26 +14,35 @@
|
||||
*/
|
||||
package org.fairscan.app.ui.screens.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.Formatter
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.fairscan.app.data.OcrLanguage
|
||||
@@ -125,3 +134,57 @@ private fun LanguageItem(
|
||||
modifier = Modifier.clickable { onInstallLanguage(lang.code) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OcrDownloadDialog(
|
||||
state: OcrDownloadUiState,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {}, // tapping outside the dialog should not cancel
|
||||
title = { Text("Downloading OCR language") }, // TODO externalize
|
||||
text = {
|
||||
Column{
|
||||
Text(state.language.displayName(Locale.current.platformLocale))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
val progress =
|
||||
state.totalBytes?.let { total ->
|
||||
state.downloadedBytes.toFloat() / total
|
||||
}
|
||||
|
||||
if (progress != null) {
|
||||
LinearProgressIndicator(progress = { progress }, Modifier.fillMaxWidth())
|
||||
} else {
|
||||
LinearProgressIndicator(Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
buildProgressText(state.downloadedBytes, state.totalBytes, LocalContext.current),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onCancel) { Text("Cancel") } // TODO externalize
|
||||
},
|
||||
confirmButton = {}
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildProgressText(
|
||||
downloadedBytes: Long,
|
||||
totalBytes: Long?,
|
||||
context: Context,
|
||||
): String {
|
||||
return if (totalBytes != null) {
|
||||
listOf(
|
||||
Formatter.formatShortFileSize(context, downloadedBytes),
|
||||
Formatter.formatShortFileSize(context, totalBytes),
|
||||
).joinToString(" / ")
|
||||
} else {
|
||||
Formatter.formatShortFileSize(context, downloadedBytes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ fun SettingsScreen(
|
||||
onExportFormatChanged: (ExportFormat) -> Unit,
|
||||
onExportQualityChanged: (ExportQuality) -> Unit,
|
||||
onInstallOcrLanguage: (String) -> Unit,
|
||||
onCancelOcrDownload: () -> Unit,
|
||||
onEnableOcrLanguage: (String, Boolean) -> Unit,
|
||||
onDeleteUnusedOcrLanguages: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
@@ -99,9 +100,11 @@ fun SettingsScreen(
|
||||
onExportFormatChanged,
|
||||
onExportQualityChanged,
|
||||
onInstallOcrLanguage,
|
||||
onCancelOcrDownload,
|
||||
onEnableOcrLanguage,
|
||||
onDeleteUnusedOcrLanguages,
|
||||
modifier = Modifier.padding(paddingValues))
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +118,7 @@ private fun SettingsContent(
|
||||
onExportFormatChanged: (ExportFormat) -> Unit,
|
||||
onExportQualityChanged: (ExportQuality) -> Unit,
|
||||
onInstallOcrLanguage: (String) -> Unit,
|
||||
onCancelOcrDownload: () -> Unit,
|
||||
onEnableOcrLanguage: (String, Boolean) -> Unit,
|
||||
onDeleteUnusedOcrLanguages: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -267,6 +271,12 @@ private fun SettingsContent(
|
||||
onConfirm = onDeleteUnusedOcrLanguages,
|
||||
)
|
||||
}
|
||||
uiState.currentDownload?.let { download ->
|
||||
OcrDownloadDialog(
|
||||
state = download,
|
||||
onCancel = onCancelOcrDownload,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -366,6 +376,7 @@ fun SettingsScreenPreview(uiState: SettingsUiState) {
|
||||
onExportFormatChanged = {},
|
||||
onExportQualityChanged = {},
|
||||
onInstallOcrLanguage = {},
|
||||
onCancelOcrDownload = {},
|
||||
onEnableOcrLanguage = { _,_->},
|
||||
onDeleteUnusedOcrLanguages = {},
|
||||
onBack = {}
|
||||
|
||||
@@ -16,6 +16,7 @@ package org.fairscan.app.ui.screens.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -24,6 +25,7 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.fairscan.app.AppContainer
|
||||
import org.fairscan.app.data.OcrLanguage
|
||||
import org.fairscan.app.domain.ExportQuality
|
||||
|
||||
data class SettingsUiState(
|
||||
@@ -31,6 +33,7 @@ data class SettingsUiState(
|
||||
val export: ExportSettingsUiState = ExportSettingsUiState(),
|
||||
val installedOcrLanguages: Set<String> = emptySet(),
|
||||
val enabledOcrLanguages: Set<String> = emptySet(),
|
||||
val currentDownload: OcrDownloadUiState? = null
|
||||
)
|
||||
|
||||
data class ExportSettingsUiState(
|
||||
@@ -40,12 +43,21 @@ data class ExportSettingsUiState(
|
||||
val quality: ExportQuality = ExportQuality.BALANCED,
|
||||
)
|
||||
|
||||
data class OcrDownloadUiState(
|
||||
val language: OcrLanguage,
|
||||
val downloadedBytes: Long = 0,
|
||||
val totalBytes: Long? = null,
|
||||
)
|
||||
|
||||
class SettingsViewModel(container: AppContainer) : ViewModel() {
|
||||
|
||||
private val repo = container.settingsRepository
|
||||
private val ocrLanguageRepo = container.ocrLanguageRepository
|
||||
|
||||
private val _installedLanguages = MutableStateFlow<Set<String>>(emptySet())
|
||||
private val _ocrDownload = MutableStateFlow<OcrDownloadUiState?>(null)
|
||||
private var downloadJob: Job? = null
|
||||
|
||||
private val _dirName = MutableStateFlow<String?>(null)
|
||||
val dirName: StateFlow<String?> = _dirName
|
||||
|
||||
@@ -64,12 +76,14 @@ class SettingsViewModel(container: AppContainer) : ViewModel() {
|
||||
exportSettingsState,
|
||||
_installedLanguages,
|
||||
ocrLanguageRepo.enabledLanguages,
|
||||
) { colorMode, exportSettings, installed, enabled ->
|
||||
_ocrDownload,
|
||||
) { colorMode, exportSettings, installed, enabled, download ->
|
||||
SettingsUiState(
|
||||
defaultColorMode = colorMode,
|
||||
export = exportSettings,
|
||||
installedOcrLanguages = installed,
|
||||
enabledOcrLanguages = enabled,
|
||||
currentDownload = download,
|
||||
)
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
@@ -121,13 +135,29 @@ class SettingsViewModel(container: AppContainer) : ViewModel() {
|
||||
}
|
||||
|
||||
fun installLanguage(code: String) {
|
||||
viewModelScope.launch {
|
||||
ocrLanguageRepo.downloadLanguage(code)
|
||||
ocrLanguageRepo.setLanguageEnabled(code, true)
|
||||
refreshInstalledLanguages()
|
||||
downloadJob?.cancel()
|
||||
downloadJob = viewModelScope.launch {
|
||||
_ocrDownload.value = OcrDownloadUiState(OcrLanguage(code))
|
||||
try {
|
||||
ocrLanguageRepo.downloadLanguage(code) { progress ->
|
||||
_ocrDownload.value =
|
||||
_ocrDownload.value?.copy(
|
||||
downloadedBytes = progress.downloadedBytes,
|
||||
totalBytes = progress.totalBytes,
|
||||
)
|
||||
}
|
||||
ocrLanguageRepo.setLanguageEnabled(code, true)
|
||||
refreshInstalledLanguages()
|
||||
} finally {
|
||||
_ocrDownload.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelOcrDownload() {
|
||||
downloadJob?.cancel()
|
||||
}
|
||||
|
||||
fun setOcrLanguageEnabled(code: String, enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
ocrLanguageRepo.setLanguageEnabled(code, enabled)
|
||||
|
||||
Reference in New Issue
Block a user