Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cab2dc742e | ||
|
|
962605e2b4 | ||
|
|
d360ecfb2b | ||
|
|
78754d2c75 | ||
|
|
3567f50ad3 | ||
|
|
c79be491cc | ||
|
|
0f3d4b965c | ||
|
|
e6f9f7f900 | ||
|
|
e5650f7247 | ||
|
|
ccbd037507 | ||
|
|
1907e3004d | ||
|
|
84800cf2b0 | ||
|
|
33cc386015 | ||
|
|
07031c7ac1 | ||
|
|
5a7ba69b28 | ||
|
|
bfe4721c6b | ||
|
|
097bb23ce9 | ||
|
|
5126c19f3c | ||
|
|
70faf71f99 | ||
|
|
9fcd7914dc | ||
|
|
bd7f597986 |
@@ -1,45 +1,16 @@
|
|||||||
package com.example.research.core.domain.model
|
package com.example.research.core.domain.model
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Snapshot of an ongoing indexing operation.
|
* Snapshot of an ongoing indexing operation.
|
||||||
*
|
*
|
||||||
* [progress] is the authoritative [0f, 1f] completion value and is O(1) to
|
* [progress] is the authoritative [0f, 1f] completion value: the repository
|
||||||
* compute: the repository's internal `ProgressTracker` maintains a running
|
* aggregates a file-size-weighted sum across files incrementally while
|
||||||
* sum across files incrementally and stores it in [aggregateSum], avoiding
|
* indexing, so reading it is O(1).
|
||||||
* an O(n) walk over [perFileProgress] on every read.
|
|
||||||
* [perFileProgress] is kept for diagnostics; callers should prefer
|
|
||||||
* [progress] / [progressPercent].
|
|
||||||
*/
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
data class IndexingProgress(
|
data class IndexingProgress(
|
||||||
val currentFile: String = "",
|
|
||||||
val currentIndex: Int = 0,
|
|
||||||
val totalFiles: Int = 0,
|
|
||||||
val isIndexing: Boolean = false,
|
val isIndexing: Boolean = false,
|
||||||
val currentFileProgress: Float = 0f,
|
val progress: Float = 0f,
|
||||||
val label: String = "",
|
)
|
||||||
val perFileProgress: Map<String, Float> = emptyMap(),
|
|
||||||
/** Pre-aggregated sum in [0f, totalFiles] supplied by the producer; -1f = unknown. */
|
|
||||||
val aggregateSum: Float = -1f,
|
|
||||||
) {
|
|
||||||
val progress: Float
|
|
||||||
get() = if (totalFiles > 0) {
|
|
||||||
when {
|
|
||||||
aggregateSum >= 0f -> (aggregateSum / totalFiles).coerceIn(0f, 1f)
|
|
||||||
perFileProgress.isNotEmpty() -> {
|
|
||||||
val totalProgress = perFileProgress.values.sum()
|
|
||||||
(totalProgress / totalFiles).coerceIn(0f, 1f)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val completedFiles = (currentIndex - 1).coerceAtLeast(0)
|
|
||||||
((completedFiles + currentFileProgress) / totalFiles).coerceIn(0f, 1f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else 0f
|
|
||||||
|
|
||||||
val progressPercent: Int
|
|
||||||
get() = (progress * 100).roundToInt().coerceIn(0, 100)
|
|
||||||
}
|
|
||||||
|
|||||||
+24
-30
@@ -70,13 +70,9 @@ private class ProgressTracker(files: List<LocalDictionaryRepository.DiscoveredFi
|
|||||||
aggregateWeightedMicros.addAndGet(newMicros - prevMicros)
|
aggregateWeightedMicros.addAndGet(newMicros - prevMicros)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun progress(): Float {
|
||||||
* Current weighted sum mapped back to [0f, totalFiles] so the existing
|
|
||||||
* IndexingProgress model can keep deriving progress as aggregate/total.
|
|
||||||
*/
|
|
||||||
fun aggregateSum(): Float {
|
|
||||||
val weightedProgress = aggregateWeightedMicros.get().toDouble() / (totalWeight.toDouble() * 1_000_000.0)
|
val weightedProgress = aggregateWeightedMicros.get().toDouble() / (totalWeight.toDouble() * 1_000_000.0)
|
||||||
return (weightedProgress.coerceIn(0.0, 1.0) * totalFiles).toFloat()
|
return weightedProgress.coerceIn(0.0, 1.0).toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
@@ -192,61 +188,59 @@ class LocalDictionaryRepository(
|
|||||||
mutableIndexingProgress.emit(
|
mutableIndexingProgress.emit(
|
||||||
IndexingProgress(
|
IndexingProgress(
|
||||||
isIndexing = true,
|
isIndexing = true,
|
||||||
currentFile = context.getString(R.string.indexing_label),
|
progress = progressTracker?.progress() ?: 0f,
|
||||||
totalFiles = filesToScan.size,
|
|
||||||
currentIndex = filesToScan.size - filesToIndex.size,
|
|
||||||
aggregateSum = progressTracker?.aggregateSum() ?: -1f,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val lastEmittedPercent = AtomicInteger(-1)
|
val lastEmittedPercent = AtomicInteger(-1)
|
||||||
val lastProgressEmitMs = AtomicLong(0L)
|
val lastProgressEmitMs = AtomicLong(0L)
|
||||||
|
|
||||||
|
val publishCurrentProgress = {
|
||||||
|
val tracker = progressTracker
|
||||||
|
if (tracker != null) {
|
||||||
|
val progress = tracker.progress()
|
||||||
|
lastEmittedPercent.set((progress * 100f).toInt().coerceIn(0, 100))
|
||||||
|
lastProgressEmitMs.set(SystemClock.elapsedRealtime())
|
||||||
|
mutableIndexingProgress.update { p ->
|
||||||
|
p.copy(progress = progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
supervisorScope {
|
supervisorScope {
|
||||||
filesToIndex.mapIndexed { index, file ->
|
filesToIndex.map { file ->
|
||||||
async {
|
async {
|
||||||
indexingSemaphore.withPermit {
|
indexingSemaphore.withPermit {
|
||||||
coroutineContext.ensureActive()
|
coroutineContext.ensureActive()
|
||||||
yield()
|
yield()
|
||||||
|
|
||||||
val res = indexDictionary(file) { _, op, prog ->
|
val res = indexDictionary(file) { _, _, prog ->
|
||||||
if (prog >= 0f) {
|
if (prog >= 0f) {
|
||||||
progressTracker?.updateFileProgress(file.name, prog)
|
progressTracker?.updateFileProgress(file.name, prog)
|
||||||
}
|
}
|
||||||
|
|
||||||
val tracker = progressTracker
|
val progress = progressTracker?.progress() ?: 0f
|
||||||
val aggregate = tracker?.aggregateSum() ?: -1f
|
val newPercent = (progress * 100f).toInt().coerceIn(0, 100)
|
||||||
val newPercent = if (filesToScan.isNotEmpty() && aggregate >= 0f) {
|
|
||||||
((aggregate / filesToScan.size) * 100f).toInt().coerceIn(0, 100)
|
|
||||||
} else -1
|
|
||||||
|
|
||||||
val currentIdx = filesToScan.size - filesToIndex.size + index + 1
|
|
||||||
val previous = mutableIndexingProgress.value
|
|
||||||
val now = SystemClock.elapsedRealtime()
|
val now = SystemClock.elapsedRealtime()
|
||||||
val percentChanged = newPercent >= 0 && newPercent != lastEmittedPercent.get()
|
val percentChanged = newPercent != lastEmittedPercent.get()
|
||||||
val fileChanged = previous.currentFile != file.name
|
|
||||||
val completed = newPercent >= 100 || prog >= 1f
|
val completed = newPercent >= 100 || prog >= 1f
|
||||||
val intervalElapsed =
|
val intervalElapsed =
|
||||||
now - lastProgressEmitMs.get() >= MIN_PROGRESS_EMIT_INTERVAL_MS
|
now - lastProgressEmitMs.get() >= MIN_PROGRESS_EMIT_INTERVAL_MS
|
||||||
val shouldEmit = fileChanged || completed || (percentChanged && intervalElapsed)
|
val shouldEmit = completed || (percentChanged && intervalElapsed)
|
||||||
|
|
||||||
if (shouldEmit) {
|
if (shouldEmit) {
|
||||||
if (newPercent >= 0) lastEmittedPercent.set(newPercent)
|
lastEmittedPercent.set(newPercent)
|
||||||
lastProgressEmitMs.set(now)
|
lastProgressEmitMs.set(now)
|
||||||
mutableIndexingProgress.update { p ->
|
mutableIndexingProgress.update { p ->
|
||||||
p.copy(
|
p.copy(progress = progress)
|
||||||
currentFile = file.name,
|
|
||||||
currentIndex = currentIdx,
|
|
||||||
currentFileProgress = prog,
|
|
||||||
label = op.ifEmpty { p.label },
|
|
||||||
aggregateSum = aggregate,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res is OperationResult.Success) {
|
if (res is OperationResult.Success) {
|
||||||
progressTracker?.updateFileProgress(file.name, 1.0f)
|
progressTracker?.updateFileProgress(file.name, 1.0f)
|
||||||
|
publishCurrentProgress()
|
||||||
totalArticlesIndexed.addAndGet(res.data.articleCount)
|
totalArticlesIndexed.addAndGet(res.data.articleCount)
|
||||||
val dictionary = res.data
|
val dictionary = res.data
|
||||||
addDictionary(dictionary)
|
addDictionary(dictionary)
|
||||||
|
|||||||
+19
-5
@@ -1,8 +1,8 @@
|
|||||||
package com.example.research.ui.settings.components
|
package com.example.research.ui.settings.components
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -19,6 +19,10 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.example.research.R
|
import com.example.research.R
|
||||||
|
|
||||||
|
private const val MILLIS_PER_PROGRESS_UNIT = 10_000
|
||||||
|
private const val MIN_PROGRESS_MOTION_MS = 50
|
||||||
|
private const val MAX_PROGRESS_MOTION_MS = 650
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DictionaryProgressSection(
|
fun DictionaryProgressSection(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
@@ -58,11 +62,21 @@ fun DictionaryProgressSection(
|
|||||||
displayedTestTag = testTag
|
displayedTestTag = testTag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val animatedProgress by animateFloatAsState(
|
val animatedState = remember { Animatable(0f) }
|
||||||
|
LaunchedEffect(monotonicTargetProgress) {
|
||||||
|
val delta = monotonicTargetProgress - animatedState.value
|
||||||
|
if (delta > 0f) {
|
||||||
|
animatedState.animateTo(
|
||||||
targetValue = monotonicTargetProgress,
|
targetValue = monotonicTargetProgress,
|
||||||
animationSpec = tween(durationMillis = 650, easing = FastOutSlowInEasing),
|
animationSpec = tween(
|
||||||
label = "dictionary-progress"
|
durationMillis = (delta * MILLIS_PER_PROGRESS_UNIT).toInt()
|
||||||
|
.coerceIn(MIN_PROGRESS_MOTION_MS, MAX_PROGRESS_MOTION_MS),
|
||||||
|
easing = LinearEasing,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val animatedProgress = animatedState.value
|
||||||
|
|
||||||
Column(modifier = Modifier.testTag(displayedTestTag)) {
|
Column(modifier = Modifier.testTag(displayedTestTag)) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
<string name="language_english">Английский</string>
|
<string name="language_english">Английский</string>
|
||||||
<string name="language_russian">Русский</string>
|
<string name="language_russian">Русский</string>
|
||||||
<string name="language_system">Системный</string>
|
<string name="language_system">Системный</string>
|
||||||
<string name="indexing_label">Индексация словарей</string>
|
|
||||||
<string name="clear_search">Очистить поиск</string>
|
<string name="clear_search">Очистить поиск</string>
|
||||||
<string name="article_no_selected">Статья не выбрана</string>
|
<string name="article_no_selected">Статья не выбрана</string>
|
||||||
<string name="article_return_to_search">Вернуться к поиску</string>
|
<string name="article_return_to_search">Вернуться к поиску</string>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
<string name="language_english">English</string>
|
<string name="language_english">English</string>
|
||||||
<string name="language_russian">Russian</string>
|
<string name="language_russian">Russian</string>
|
||||||
<string name="language_system">System</string>
|
<string name="language_system">System</string>
|
||||||
<string name="indexing_label">Indexing dictionaries</string>
|
|
||||||
<string name="clear_search">Clear search</string>
|
<string name="clear_search">Clear search</string>
|
||||||
<string name="article_no_selected">No article selected</string>
|
<string name="article_no_selected">No article selected</string>
|
||||||
<string name="article_return_to_search">Return to search</string>
|
<string name="article_return_to_search">Return to search</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user