Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d18d0c927 | ||
|
|
69bbb97b2a | ||
|
|
c8adf537d4 |
@@ -92,8 +92,8 @@ android {
|
||||
applicationId = "com.example.research"
|
||||
minSdk = project.property("minSdk").toString().toInt()
|
||||
targetSdk = project.property("targetSdk").toString().toInt()
|
||||
versionCode = 4
|
||||
versionName = "1.2.0"
|
||||
versionCode = 6
|
||||
versionName = "1.3.0"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
@@ -114,12 +114,9 @@ android {
|
||||
abiFilters += "arm64-v8a"
|
||||
}
|
||||
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
optimization {
|
||||
enable = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ class DownloadManager(
|
||||
mutableDownloadProgressState.asStateFlow()
|
||||
private val mutex = Mutex()
|
||||
private var downloadJob: Job? = null
|
||||
private var cancelCleanupJob: Job? = null
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DownloadManager"
|
||||
@@ -44,6 +45,7 @@ class DownloadManager(
|
||||
fun startDownload() {
|
||||
if (downloadState.value is DownloadState.Loading) return
|
||||
if (!downloadScope.isActive) return
|
||||
cancelCleanupJob = null
|
||||
downloadJob = downloadScope.launch {
|
||||
mutex.withLock {
|
||||
try {
|
||||
@@ -94,7 +96,7 @@ class DownloadManager(
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) {
|
||||
updateDownloadProgressState(DownloadState.Cancelled, 0f)
|
||||
downloadScope.launch {
|
||||
cancelCleanupJob = downloadScope.launch {
|
||||
try {
|
||||
dictionaryRepository.deleteUnprocessedFiles()
|
||||
} catch (cleanupException: Exception) {
|
||||
@@ -124,6 +126,7 @@ class DownloadManager(
|
||||
fun startDownloadForSources(sourceUrls: List<String>) {
|
||||
if (downloadState.value is DownloadState.Loading) return
|
||||
if (!downloadScope.isActive) return
|
||||
cancelCleanupJob = null
|
||||
downloadJob = downloadScope.launch {
|
||||
mutex.withLock {
|
||||
try {
|
||||
@@ -168,7 +171,7 @@ class DownloadManager(
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) {
|
||||
updateDownloadProgressState(DownloadState.Cancelled, 0f)
|
||||
downloadScope.launch {
|
||||
cancelCleanupJob = downloadScope.launch {
|
||||
try {
|
||||
dictionaryRepository.deleteUnprocessedFiles()
|
||||
} catch (cleanupException: Exception) {
|
||||
@@ -195,10 +198,12 @@ class DownloadManager(
|
||||
}
|
||||
}
|
||||
fun cancelDownload() {
|
||||
downloadJob?.cancel()
|
||||
val jobToCancel = downloadJob
|
||||
jobToCancel?.cancel()
|
||||
updateDownloadProgressState(DownloadState.Cancelled, 0f)
|
||||
downloadScope.launch {
|
||||
delay(TERMINAL_STATE_DURATION)
|
||||
jobToCancel?.join()
|
||||
cancelCleanupJob?.join()
|
||||
if (downloadState.value == DownloadState.Cancelled) {
|
||||
updateDownloadProgressState(DownloadState.Idle, 0f)
|
||||
}
|
||||
|
||||
+2
@@ -343,6 +343,8 @@ class DictionaryDownloader(
|
||||
context.getString(R.string.download_connection_reset)
|
||||
e is SecurityException ->
|
||||
context.getString(R.string.download_no_write_permission)
|
||||
e.isNetworkError() ->
|
||||
context.getString(R.string.download_network_failed)
|
||||
else ->
|
||||
context.getString(R.string.download_error)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
@@ -135,7 +136,9 @@ private fun SearchField(
|
||||
Icon(
|
||||
painter = searchIcon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
modifier = Modifier
|
||||
.offset(x = 4.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
},
|
||||
trailingIcon = if (searchState.text.isNotEmpty()) {
|
||||
@@ -146,6 +149,7 @@ private fun SearchField(
|
||||
onClearQuery()
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.testTag("clear_search")
|
||||
.semantics { testTagsAsResourceId = true }
|
||||
) {
|
||||
|
||||
+40
-18
@@ -30,6 +30,7 @@ import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
@@ -81,7 +82,8 @@ fun DictionaryListItem(
|
||||
dictionary: Dictionary,
|
||||
onToggle: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
canDelete: Boolean = true
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val density = LocalDensity.current
|
||||
@@ -93,7 +95,9 @@ fun DictionaryListItem(
|
||||
val offsetAnim = remember { Animatable(0f) }
|
||||
|
||||
var rawOffset by remember { mutableFloatStateOf(0f) }
|
||||
val isDeleteRevealed by remember { derivedStateOf { rawOffset <= -swipeThresholdPx } }
|
||||
val isDeleteRevealed by remember(canDelete) {
|
||||
derivedStateOf { canDelete && rawOffset <= -swipeThresholdPx }
|
||||
}
|
||||
|
||||
LaunchedEffect(isDeleteRevealed) {
|
||||
if (isDeleteRevealed) {
|
||||
@@ -101,6 +105,13 @@ fun DictionaryListItem(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(canDelete) {
|
||||
if (!canDelete) {
|
||||
rawOffset = 0f
|
||||
offsetAnim.snapTo(0f)
|
||||
}
|
||||
}
|
||||
|
||||
val currentOnDelete by rememberUpdatedState(onDelete)
|
||||
val currentOnToggle by rememberUpdatedState(onToggle)
|
||||
|
||||
@@ -121,6 +132,8 @@ fun DictionaryListItem(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(DictionaryItemHeight)
|
||||
.clipToBounds()
|
||||
.testTag("dictionary_item")
|
||||
) {
|
||||
Row(
|
||||
@@ -129,7 +142,8 @@ fun DictionaryListItem(
|
||||
.height(DictionaryItemHeight)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.graphicsLayer { translationX = offsetAnim.value }
|
||||
.pointerInput(maxSwipePx, swipeThresholdPx) {
|
||||
.pointerInput(canDelete, maxSwipePx, swipeThresholdPx) {
|
||||
if (!canDelete) return@pointerInput
|
||||
detectHorizontalDragGestures(
|
||||
onDragStart = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
@@ -222,25 +236,11 @@ fun DictionaryListItem(
|
||||
}
|
||||
|
||||
if (isDeleteRevealed) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
currentOnDelete()
|
||||
scope.launch { offsetAnim.snapTo(0f) }
|
||||
rawOffset = 0f
|
||||
},
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(end = ItemHorizontalPadding)
|
||||
.graphicsLayer { translationX = -offsetAnim.value }
|
||||
.size(IconButtonSize)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_delete),
|
||||
contentDescription = stringResource(R.string.dictionary_delete),
|
||||
tint = DeleteIconColor,
|
||||
modifier = Modifier.size(IconSize)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Switch(
|
||||
checked = dictionary.isActive,
|
||||
@@ -257,5 +257,27 @@ fun DictionaryListItem(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isDeleteRevealed) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
currentOnDelete()
|
||||
scope.launch { offsetAnim.snapTo(0f) }
|
||||
rawOffset = 0f
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = ItemHorizontalPadding)
|
||||
.size(IconButtonSize)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_delete),
|
||||
contentDescription = stringResource(R.string.dictionary_delete),
|
||||
tint = DeleteIconColor,
|
||||
modifier = Modifier.size(IconSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -166,7 +166,8 @@ fun DictionaryManagement(
|
||||
onDeleteDictionary = onDeleteDictionary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(backgroundModifier)
|
||||
.then(backgroundModifier),
|
||||
canDelete = !isInProgress
|
||||
)
|
||||
|
||||
if (!isInProgress) {
|
||||
@@ -246,7 +247,8 @@ private fun DictionaryListSection(
|
||||
dictionaries: List<Dictionary>,
|
||||
onToggleDictionary: (String) -> Unit,
|
||||
onDeleteDictionary: (Dictionary) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
canDelete: Boolean = true
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
@@ -256,7 +258,8 @@ private fun DictionaryListSection(
|
||||
DictionaryListItem(
|
||||
dictionary = dictionary,
|
||||
onToggle = { onToggleDictionary(dictionary.path) },
|
||||
onDelete = { onDeleteDictionary(dictionary) }
|
||||
onDelete = { onDeleteDictionary(dictionary) },
|
||||
canDelete = canDelete
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="button_cancel_download">Отмена</string>
|
||||
<string name="action_ok">ОК</string>
|
||||
<string name="download_error">Ошибка загрузки словарей</string>
|
||||
<string name="download_network_failed">Нет связи с сервером словарей. Проверьте сеть и попробуйте снова</string>
|
||||
<string name="language">Язык</string>
|
||||
<string name="language_english">Английский</string>
|
||||
<string name="language_russian">Русский</string>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="button_cancel_download">Cancel</string>
|
||||
<string name="action_ok">OK</string>
|
||||
<string name="download_error">Dictionary download failed</string>
|
||||
<string name="download_network_failed">No connection to the dictionary server. Check your network and try again</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="language_english">English</string>
|
||||
<string name="language_russian">Russian</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- Fixed the dictionary update card taking too long to reappear after cancelling a download
|
||||
@@ -0,0 +1,2 @@
|
||||
- Dictionaries can no longer be deleted while an update is running, which used to lose their update source
|
||||
- Fixed the dictionary icon sliding outside the row while swiping to delete
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1 @@
|
||||
- Исправлена лишняя задержка перед повторным появлением карточки обновления словаря после отмены загрузки
|
||||
@@ -0,0 +1,2 @@
|
||||
- Словари больше нельзя удалить во время обновления — раньше при этом терялся источник обновления
|
||||
- Исправлен выход иконки словаря за границы строки при свайпе
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
aboutlibraries = "15.0.4"
|
||||
activity_compose = "1.13.0"
|
||||
agp = "9.3.0"
|
||||
agp = "9.3.1"
|
||||
compose_bom = "2026.06.01"
|
||||
core = "1.19.0"
|
||||
datastore_preferences = "1.2.1"
|
||||
|
||||
Reference in New Issue
Block a user