diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/helpers/ProgressWorkManager.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/helpers/ProgressWorkManager.kt index 8e6d4e3e..48399d93 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/helpers/ProgressWorkManager.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/helpers/ProgressWorkManager.kt @@ -14,11 +14,18 @@ fun Progress.toWorkData(): Data = .putInt(KEY_TOTAL, total) .build() -fun Progress.Companion.fromWorkData(data: Data): Progress = - Progress( - current = data.getInt(KEY_CURRENT, 0), - total = data.getInt(KEY_TOTAL, -1), +fun Progress.Companion.fromWorkData(data: Data): Progress? { + with(data.keyValueMap) { + if (isEmpty()) return null + if (!containsKey(KEY_CURRENT)) return null + if (!containsKey(KEY_TOTAL)) return null + } + + return Progress( + current = data.getInt(KEY_CURRENT, Progress.INDETERMINATE.current), + total = data.getInt(KEY_TOTAL, Progress.INDETERMINATE.total), ) +} fun Progress.Companion.fromWorkInfo(workInfo: WorkInfo?): Progress? = when (workInfo?.state) { diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/ImageHashesDatabaseBuilderJob.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/ImageHashesDatabaseBuilderJob.kt index e13dc346..d1256fe1 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/ImageHashesDatabaseBuilderJob.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/ImageHashesDatabaseBuilderJob.kt @@ -6,17 +6,23 @@ import androidx.sqlite.driver.bundled.SQLITE_OPEN_CREATE import androidx.sqlite.driver.bundled.SQLITE_OPEN_READWRITE import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import androidx.work.workDataOf import co.touchlab.kermit.Logger -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.io.files.SystemFileSystem +import xyz.sevive.arcaeaoffline.core.Progress import xyz.sevive.arcaeaoffline.core.ocr.ImageHashesDatabaseBuilder import xyz.sevive.arcaeaoffline.data.OcrDependencyPaths import xyz.sevive.arcaeaoffline.helpers.ArcaeaPackageHelper +import xyz.sevive.arcaeaoffline.helpers.toWorkData +import kotlin.time.Duration.Companion.milliseconds class ImageHashesDatabaseBuilderJob( context: Context, @@ -27,45 +33,55 @@ class ImageHashesDatabaseBuilderJob( companion object { const val NAME = "ImageHashesDatabaseBuilderJob" private const val LOG_TAG = "ImageHashesDbBuilderJob" - const val KEY_PROGRESS = "progress" - const val KEY_PROGRESS_TOTAL = "progressTotal" } + private val progressFlow = MutableStateFlow(Progress.INDETERMINATE) + override suspend fun doWork(): Result { val ocrDependencyPaths = OcrDependencyPaths() val path = ocrDependencyPaths.imageHashesDatabaseFile val arcaeaPackageHelper = ArcaeaPackageHelper(applicationContext) - val collectScope = CoroutineScope(Dispatchers.Default) - try { - withContext(Dispatchers.IO) { - if (SystemFileSystem.exists(path)) SystemFileSystem.delete(path) - SystemFileSystem.createDirectories(path.parent ?: error("$path has no parent")) - } - - BundledSQLiteDriver() - .open( - path.toString(), - SQLITE_OPEN_READWRITE.or(SQLITE_OPEN_CREATE), - ).use { conn -> - val builder = ImageHashesDatabaseBuilder(conn) - - collectScope.launch { - builder.buildProgress.collectLatest { - setProgress( - workDataOf(KEY_PROGRESS to it?.first, KEY_PROGRESS_TOTAL to it?.second), - ) - } + return coroutineScope { + @OptIn(FlowPreview::class) + val progressPublishJob = + launch { + progressFlow + .sample(100.milliseconds) + .collect { setProgress(it.toWorkData()) } } - arcaeaPackageHelper.fillHashesDatabaseBuilderTasks(builder) - builder.build(hashSize = 16, highFreqFactor = 4) - arcaeaPackageHelper.buildHashesDatabaseCleanUp() + withContext(Dispatchers.IO) { + if (SystemFileSystem.exists(path)) SystemFileSystem.delete(path) + SystemFileSystem.createDirectories(path.parent ?: error("$path has no parent")) } - return Result.success() + BundledSQLiteDriver() + .open( + path.toString(), + SQLITE_OPEN_READWRITE.or(SQLITE_OPEN_CREATE), + ).use { conn -> + val builder = ImageHashesDatabaseBuilder(conn) + + val progressUpdateJob = + launch { + builder.buildProgress.collect { progressFlow.value = it ?: Progress.INDETERMINATE } + } + + arcaeaPackageHelper.fillHashesDatabaseBuilderTasks(builder) + builder.build(hashSize = 16, highFreqFactor = 4) + arcaeaPackageHelper.buildHashesDatabaseCleanUp() + + progressUpdateJob.cancelAndJoin() + } + + progressPublishJob.cancelAndJoin() + Result.success() + } } catch (e: Exception) { + if (e is CancellationException) throw e + logger.e(e) { "Error building image hashes database" } return Result.failure() } diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/OcrQueueProcessingJob.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/OcrQueueProcessingJob.kt index 214a52a3..042a6de3 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/OcrQueueProcessingJob.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/OcrQueueProcessingJob.kt @@ -66,7 +66,7 @@ class OcrQueueProcessingJob( ; companion object { - fun fromInt(value: Int) = entries.firstOrNull { it.value == value } ?: NORMAL + fun fromInt(value: Int) = entries.firstOrNull { it.value == value } } } @@ -75,9 +75,16 @@ class OcrQueueProcessingJob( val parallelCount: Int, ) + private fun parseRunMode(): RunMode { + val runModeInput = inputData.getInt(DATA_RUN_MODE, 0) + val result = RunMode.fromInt(runModeInput) + if (result == null) logger.w { "Invalid RunMode $runModeInput, falling back to ${RunMode.NORMAL}" } + return result ?: RunMode.NORMAL + } + private fun getWorkOptions(): WorkOptions = WorkOptions( - runMode = RunMode.fromInt(inputData.getInt(DATA_RUN_MODE, RunMode.NORMAL.value)), + runMode = parseRunMode(), parallelCount = inputData .getInt( diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/R30UpdateJob.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/R30UpdateJob.kt index c49c320f..e633413a 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/R30UpdateJob.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/jobs/R30UpdateJob.kt @@ -5,17 +5,18 @@ import androidx.room.immediateTransaction import androidx.room.useWriterConnection import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import androidx.work.workDataOf import co.touchlab.kermit.Logger import io.sentry.Sentry -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import xyz.sevive.arcaeaoffline.core.Progress import xyz.sevive.arcaeaoffline.core.constants.ArcaeaPlayResultClearType import xyz.sevive.arcaeaoffline.core.constants.ArcaeaPlayResultModifier import xyz.sevive.arcaeaoffline.core.database.ArcaeaOfflineDatabase @@ -28,18 +29,19 @@ import xyz.sevive.arcaeaoffline.core.database.repositories.PropertyRepository import xyz.sevive.arcaeaoffline.core.database.repositories.R30EntryCombined import xyz.sevive.arcaeaoffline.core.database.repositories.R30EntryRepository import xyz.sevive.arcaeaoffline.core.database.repositories.SongRepository +import xyz.sevive.arcaeaoffline.helpers.toWorkData import kotlin.time.Clock -private fun PlayResult.matchesR30Condition(): Boolean { +private fun PlayResult.triggersConditionalWrite(): Boolean { // score >= EX - if (score >= 9800000) return true + if (score >= 9_800_000) return true // is hard lost if (clearType == ArcaeaPlayResultClearType.TRACK_LOST && modifier == ArcaeaPlayResultModifier.HARD) return true return false } -private fun List.minPlayRatingItem(): R30EntryCombined? = +private fun List.minByPlayRating(): R30EntryCombined? = this.minByOrNull { entry -> entry.chartInfo?.let { entry.playResult.playRating(it) } ?: Double.MAX_VALUE } @@ -59,88 +61,90 @@ class R30UpdateJob( const val WORK_NAME = "R30UpdateJob" const val DATA_RUN_MODE = "run_mode" - - const val KEY_PROGRESS = "progress" - const val KEY_PROGRESS_TOTAL = "progress_total" } + private val logger = Logger.withTag(LOG_TAG) + enum class RunMode( val value: Int, ) { NORMAL(0), - REBUILD(1), + REBUILD(1), ; + + companion object { + fun fromInt(value: Int) = entries.firstOrNull { it.value == value } + } } private data class WorkOptions( val runMode: RunMode, ) - private val logger = Logger.withTag(LOG_TAG) + private fun parseRunMode(): RunMode { + val runModeInput = inputData.getInt(DATA_RUN_MODE, 0) + val result = RunMode.fromInt(runModeInput) + if (result == null) logger.w { "Invalid RunMode $runModeInput, falling back to ${RunMode.NORMAL}" } + return result ?: RunMode.NORMAL + } private fun getWorkOptions(): WorkOptions = WorkOptions( - runMode = - RunMode.entries.firstOrNull { - it.value == inputData.getInt(DATA_RUN_MODE, 0) - } ?: RunMode.NORMAL, + runMode = parseRunMode(), ) - private val progress = MutableStateFlow(0) - private val progressTotal = MutableStateFlow(-1) - private val progressListenScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + private val progressFlow = MutableStateFlow(Progress.INDETERMINATE) override suspend fun doWork(): Result { - progressListenScope.launch { - combine(progress, progressTotal) { p, t -> p to t }.collectLatest { - setProgress( - workDataOf(KEY_PROGRESS to it.first, KEY_PROGRESS_TOTAL to it.second), - ) - } - } - val workOptions = getWorkOptions() try { - val r30LastUpdatedAt = propertyRepo.r30LastUpdatedAt() - - var r30EntryCombinedList = - when (workOptions.runMode) { - RunMode.REBUILD -> emptyList() - else -> r30EntryRepo.findAllCombined().firstOrNull() ?: emptyList() + return coroutineScope { + val progressPublishJob = + launch { + progressFlow.collectLatest { setProgress(it.toWorkData()) } + } + + val r30LastUpdatedAt = propertyRepo.r30LastUpdatedAt() + + var r30EntryCombinedList = + when (workOptions.runMode) { + RunMode.REBUILD -> emptyList() + else -> r30EntryRepo.findAllCombined().firstOrNull() ?: emptyList() + } + + val playResults = + when (workOptions.runMode) { + RunMode.REBUILD -> playResultRepo.findAll().firstOrNull() + else -> r30LastUpdatedAt?.let { playResultRepo.findLaterThan(it).firstOrNull() } + } ?: emptyList() + val deletedSongIds = songRepo.findDeletedInGame().firstOrNull()?.map { it.id } ?: emptyList() + val newPlayResults = playResults.filter { it.date != null && it.songId !in deletedSongIds }.sortedBy { it.date } + + progressFlow.update { Progress(current = 0, total = newPlayResults.size) } + logger.d { "Updating r30 list with ${newPlayResults.size} new play results" } + newPlayResults.forEach { + ensureActive() + r30EntryCombinedList = updateR30List(it, r30EntryCombinedList) + progressFlow.update { progress -> progress.increment() } } - val playResults = - when (workOptions.runMode) { - RunMode.REBUILD -> playResultRepo.findAll().firstOrNull() - else -> r30LastUpdatedAt?.let { playResultRepo.findLaterThan(it).firstOrNull() } - } ?: emptyList() - val deletedSongIds = - songRepo.findDeletedInGame().firstOrNull()?.map { it.id } ?: emptyList() - val newPlayResults = - playResults - .filter { it.date != null && it.songId !in deletedSongIds } - .sortedBy { it.date } - - progressTotal.value = newPlayResults.size - logger.d { "Updating r30 list with ${newPlayResults.size} new play results" } - newPlayResults.forEach { - r30EntryCombinedList = updateR30List(it, r30EntryCombinedList) - progress.value += 1 - } + // Room3 possibly has a convenient extension function for this + // see https://issuetracker.google.com/issues/416306996 + db.useWriterConnection { transactor -> + transactor.immediateTransaction { + r30EntryRepo.deleteAll() + r30EntryRepo.insertBatch(*r30EntryCombinedList.map { it.entry }.toTypedArray()) - // Room3 possibly has a convenient extension function for this - // see https://issuetracker.google.com/issues/416306996 - db.useWriterConnection { transactor -> - transactor.immediateTransaction { - r30EntryRepo.deleteAll() - r30EntryRepo.insertBatch(*r30EntryCombinedList.map { it.entry }.toTypedArray()) - - propertyRepo.setR30LastUpdatedAt(Clock.System.now()) + propertyRepo.setR30LastUpdatedAt(Clock.System.now()) + } } + + progressPublishJob.cancelAndJoin() + Result.success() } + } catch (e: Exception) { + if (e is CancellationException) throw e - return Result.success() - } catch (e: Throwable) { logger.e(e) { "Error updating r30" } Sentry.captureException(e) return Result.failure() @@ -148,7 +152,7 @@ class R30UpdateJob( } /** - * Wrapper of [updateR30ListByDate] and [updateR30ListByPotential] that automatically + * Wrapper of [updateR30ListByDirectWrite] and [updateR30ListByConditionalWrite] that automatically * choose one of them depending on the [playResult]'s state. */ private suspend fun updateR30List( @@ -162,22 +166,19 @@ class R30UpdateJob( } val newR30Entries = - if (playResult.matchesR30Condition()) { + if (playResult.triggersConditionalWrite()) { // now check if the play result play rating is higher than the lowest play rating r30 entry // if any chart info is missing, return the old r30 entries directly val chartInfo = chartInfoRepo.find(playResult).firstOrNull() ?: return oldR30List - updateR30ListByPotential(playResult, chartInfo, oldR30List) + updateR30ListByConditionalWrite(playResult, chartInfo, oldR30List) } else { // otherwise, just update the entries by date - updateR30ListByDate(playResult, oldR30List) + updateR30ListByDirectWrite(playResult, oldR30List) } // ensure the new r30 should have at least 10 unique charts // otherwise keep the entries unmodified - val uniqueChartsCount = - newR30Entries - .distinctBy { "${it.playResult.songId}|${it.playResult.ratingClass.value}" } - .count() + val uniqueChartsCount = newR30Entries.distinctBy { "${it.playResult.songId}|${it.playResult.ratingClass.value}" }.count() return if (uniqueChartsCount < 10) { oldR30List } else { @@ -195,14 +196,14 @@ class R30UpdateJob( * @param oldR30List Old R30 entries * @return The new R30 entries */ - private fun updateR30ListByPotential( + private fun updateR30ListByConditionalWrite( playResult: PlayResult, chartInfo: ChartInfo, oldR30List: List, ): List { // try getting the min play rating item in old list // otherwise leave the old list untouched - val minRatingEntry = oldR30List.minPlayRatingItem() ?: return oldR30List + val minRatingEntry = oldR30List.minByPlayRating() ?: return oldR30List val minRatingEntryRating = minRatingEntry.playRating() ?: return oldR30List if (playResult.playRating(chartInfo) < minRatingEntryRating) return oldR30List @@ -222,7 +223,7 @@ class R30UpdateJob( * @param oldR30List Old R30 entries * @return The new R30 entries */ - private suspend fun updateR30ListByDate( + private suspend fun updateR30ListByDirectWrite( playResult: PlayResult, oldR30List: List, ): List { diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/LinearProgressIndicatorWrapper.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/LinearProgressIndicatorWrapper.kt index 108e269a..39a1d6d9 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/LinearProgressIndicatorWrapper.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/LinearProgressIndicatorWrapper.kt @@ -99,22 +99,3 @@ fun LinearProgressIndicatorWrapper( formatter = formatter, ) } - -@Deprecated("Migrate to new Progress data class") -@Composable -fun LinearProgressIndicatorWrapper( - progress: Pair?, - modifier: Modifier = Modifier, - indeterminateLabel: String? = LinearProgressIndicatorWrapperDefaults.indeterminateLabel, - determinateLabel: String? = LinearProgressIndicatorWrapperDefaults.determinateLabel, - formatter: PercentageFormatter = LinearProgressIndicatorWrapperDefaults.formatter, -) { - LinearProgressIndicatorWrapper( - current = progress?.first ?: 0, - total = progress?.second ?: -1, - modifier = modifier, - indeterminateLabel = indeterminateLabel, - determinateLabel = determinateLabel, - formatter = formatter, - ) -} diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ocr/OcrDependencyImageHashesDatabaseStatusViewer.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ocr/OcrDependencyImageHashesDatabaseStatusViewer.kt index 659df437..eda183bc 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ocr/OcrDependencyImageHashesDatabaseStatusViewer.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ocr/OcrDependencyImageHashesDatabaseStatusViewer.kt @@ -7,11 +7,12 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import xyz.sevive.arcaeaoffline.R +import xyz.sevive.arcaeaoffline.core.Progress import xyz.sevive.arcaeaoffline.helpers.ImageHashesDatabaseStatusDetail import xyz.sevive.arcaeaoffline.ui.components.LinearProgressIndicatorWrapper data class OcrDependencyImageHashesDatabaseStatusUiState( - val progress: Pair? = null, + val progress: Progress? = null, val statusDetail: ImageHashesDatabaseStatusDetail = ImageHashesDatabaseStatusDetail(), ) diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/database/r30list/DatabaseR30ListScreen.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/database/r30list/DatabaseR30ListScreen.kt index b0d6ae18..d0179510 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/database/r30list/DatabaseR30ListScreen.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/database/r30list/DatabaseR30ListScreen.kt @@ -3,9 +3,11 @@ package xyz.sevive.arcaeaoffline.ui.screens.database.r30list import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons @@ -43,15 +45,6 @@ import xyz.sevive.arcaeaoffline.ui.components.dialogs.DialogDismissTextButton import xyz.sevive.arcaeaoffline.ui.navigation.DatabaseSubScreen import xyz.sevive.arcaeaoffline.ui.screens.EmptyScreen -@Composable -private fun DatabaseR30UpdateProgress( - current: Int, - total: Int, - modifier: Modifier = Modifier, -) { - LinearProgressIndicatorWrapper(current = current, total = total, modifier) -} - @Composable private fun DatabaseR30RebuildConfirmDialog( onDismiss: () -> Unit, @@ -83,7 +76,7 @@ private fun DatabaseR30RebuildConfirmDialog( @Composable internal fun DatabaseR30ListScreen(viewModel: DatabaseR30ListViewModel = koinViewModel()) { val updateProgress by viewModel.updateProgress.collectAsStateWithLifecycle() - val isUpdating by remember { derivedStateOf { updateProgress.second > -1 } } + val isUpdating by remember { derivedStateOf { updateProgress != null } } val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiItems = uiState.listItems @@ -136,10 +129,9 @@ internal fun DatabaseR30ListScreen(viewModel: DatabaseR30ListViewModel = koinVie ) { if (isUpdating) { Box(Modifier.fillMaxSize()) { - DatabaseR30UpdateProgress( - current = updateProgress.first, - total = updateProgress.second, - modifier = Modifier.align(Alignment.Center), + LinearProgressIndicatorWrapper( + progress = updateProgress, + Modifier.align(Alignment.Center).width(IntrinsicSize.Min), ) } } else if (uiState.isLoading) { diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/database/r30list/DatabaseR30ListViewModel.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/database/r30list/DatabaseR30ListViewModel.kt index 99d0acb3..4fbb6c20 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/database/r30list/DatabaseR30ListViewModel.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/database/r30list/DatabaseR30ListViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transform +import xyz.sevive.arcaeaoffline.core.Progress import xyz.sevive.arcaeaoffline.core.database.entities.Chart import xyz.sevive.arcaeaoffline.core.database.entities.PlayResult import xyz.sevive.arcaeaoffline.core.database.entities.R30Entry @@ -22,6 +23,7 @@ import xyz.sevive.arcaeaoffline.core.database.repositories.DifficultyRepository import xyz.sevive.arcaeaoffline.core.database.repositories.PropertyRepository import xyz.sevive.arcaeaoffline.core.database.repositories.R30EntryRepository import xyz.sevive.arcaeaoffline.core.database.repositories.SongRepository +import xyz.sevive.arcaeaoffline.helpers.fromWorkInfo import xyz.sevive.arcaeaoffline.jobs.R30UpdateJob import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant @@ -96,18 +98,11 @@ class DatabaseR30ListViewModel( val updateProgress = workManager .getWorkInfosForUniqueWorkFlow(R30UpdateJob.WORK_NAME) - .mapLatest { workInfos -> - val workInfo = workInfos.getOrNull(0) ?: return@mapLatest 0 to -1 - - workInfo.progress.getInt(R30UpdateJob.KEY_PROGRESS, 0) to - workInfo.progress.getInt( - R30UpdateJob.KEY_PROGRESS_TOTAL, - -1, - ) - }.stateIn( + .mapLatest { workInfos -> Progress.fromWorkInfo(workInfos.getOrNull(0)) } + .stateIn( viewModelScope, SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), - 0 to -1, + null, ) private fun enqueueWork(runMode: R30UpdateJob.RunMode) { diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/ocr/dependencies/OcrDependenciesScreenViewModel.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/ocr/dependencies/OcrDependenciesScreenViewModel.kt index b6aa4be6..78e0e95e 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/ocr/dependencies/OcrDependenciesScreenViewModel.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/ocr/dependencies/OcrDependenciesScreenViewModel.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.launch import kotlinx.io.buffered import kotlinx.io.files.SystemFileSystem import org.opencv.ml.KNearest +import xyz.sevive.arcaeaoffline.core.Progress import xyz.sevive.arcaeaoffline.core.ocr.ImageHashesDatabase import xyz.sevive.arcaeaoffline.data.OcrDependencyPaths import xyz.sevive.arcaeaoffline.helpers.ArcaeaResourcesStateHolder @@ -30,6 +31,7 @@ import xyz.sevive.arcaeaoffline.helpers.OcrDependencyLoader import xyz.sevive.arcaeaoffline.helpers.OcrDependencyStatusBuilder import xyz.sevive.arcaeaoffline.helpers.context.copyToCache import xyz.sevive.arcaeaoffline.helpers.context.getFileSize +import xyz.sevive.arcaeaoffline.helpers.fromWorkInfo import xyz.sevive.arcaeaoffline.jobs.ImageHashesDatabaseBuilderJob import xyz.sevive.arcaeaoffline.ui.components.ocr.OcrDependencyCrnnModelStatusUiState import xyz.sevive.arcaeaoffline.ui.components.ocr.OcrDependencyImageHashesDatabaseStatusUiState @@ -69,14 +71,8 @@ class OcrDependenciesScreenViewModel( private val imagesHashesDatabaseBuildProgress = imageHashesDatabaseBuilderJobInfo - .map { - if (it == null) return@map null - - val progress = it.progress.getInt(ImageHashesDatabaseBuilderJob.KEY_PROGRESS, -1) - val total = it.progress.getInt(ImageHashesDatabaseBuilderJob.KEY_PROGRESS_TOTAL, -1) - - if (progress == -1) null else progress to total - }.stateIn(viewModelScope, sharingStarted, null) + .map { Progress.fromWorkInfo(it) } + .stateIn(viewModelScope, sharingStarted, null) private val imagesHashesDatabaseStatusDetail = MutableStateFlow(ImageHashesDatabaseStatusDetail()) diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/Progress.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/Progress.kt index 5ad035b5..72371b1c 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/Progress.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/Progress.kt @@ -14,7 +14,7 @@ data class Progress( (current.toFloat() / total).coerceIn(0f, 1f) } - fun increment() = copy(current = current + 1) + fun increment() = copy(current = (current + 1).coerceAtMost(total)) companion object { val INDETERMINATE = Progress() diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/repositories/R30EntryRepository.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/repositories/R30EntryRepository.kt index 90207669..d9a637cd 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/repositories/R30EntryRepository.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/repositories/R30EntryRepository.kt @@ -1,9 +1,6 @@ package xyz.sevive.arcaeaoffline.core.database.repositories import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull import xyz.sevive.arcaeaoffline.core.database.daos.R30EntryDao @@ -41,9 +38,6 @@ data class R30EntryCombined( } interface R30EntryRepository { - val updating: StateFlow - val updateProgress: StateFlow> - fun findAll(): Flow> fun findAllCombined(): Flow> @@ -62,12 +56,6 @@ class R30EntryRepositoryImpl( const val LOG_TAG = "R30EntryRepoImpl" } - private val _updating = MutableStateFlow(false) - override val updating = _updating.asStateFlow() - - private val _updateProgress = MutableStateFlow(0 to -1) - override val updateProgress: StateFlow> = _updateProgress.asStateFlow() - override fun findAll(): Flow> = dao.findAll() override suspend fun insertBatch(vararg items: R30Entry) = dao.insertBatch(*items) diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/ocr/ImageHashesDatabaseBuilder.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/ocr/ImageHashesDatabaseBuilder.kt index 4edbb077..17b9c2be 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/ocr/ImageHashesDatabaseBuilder.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/ocr/ImageHashesDatabaseBuilder.kt @@ -5,8 +5,10 @@ import androidx.sqlite.execSQL import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.io.files.Path import org.opencv.core.Mat +import xyz.sevive.arcaeaoffline.core.Progress import kotlin.time.Clock import kotlin.time.Instant @@ -26,17 +28,17 @@ class ImageHashesDatabaseBuilder( val inputToGrayscaleImage: (T) -> Mat, ) - private val _buildProgress = MutableStateFlow?>(null) + private val _buildProgress = MutableStateFlow(null) val buildProgress = _buildProgress.asStateFlow() private val tasks = mutableListOf>() private fun initBuildProgress() { - _buildProgress.value = 0 to tasks.size + _buildProgress.update { Progress(current = 0, total = tasks.size) } } private fun increaseBuildProgress() { - _buildProgress.value = _buildProgress.value?.let { it.copy(first = it.first + 1) } + _buildProgress.update { it?.increment() } } private fun resetBuildProgress() {