diff --git a/app/src/main/java/com/example/research/common/util/NotificationHelper.kt b/app/src/main/java/com/example/research/common/util/NotificationHelper.kt index e5eb937..b2041dc 100644 --- a/app/src/main/java/com/example/research/common/util/NotificationHelper.kt +++ b/app/src/main/java/com/example/research/common/util/NotificationHelper.kt @@ -59,13 +59,12 @@ class NotificationHelper(private val context: Context) : NotificationPort { val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager manager?.createNotificationChannel(channel) } - private fun canShowNotification(): Boolean { - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) return true - return ContextCompat.checkSelfPermission( - context, - android.Manifest.permission.POST_NOTIFICATIONS - ) == android.content.pm.PackageManager.PERMISSION_GRANTED - } + private fun canShowNotification(): Boolean = + android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU || + ContextCompat.checkSelfPermission( + context, + android.Manifest.permission.POST_NOTIFICATIONS + ) == android.content.pm.PackageManager.PERMISSION_GRANTED override fun showProgress(percent: Int, titleRes: Int) { showUnifiedProgressNotification( title = context.getString(titleRes), diff --git a/app/src/main/java/com/example/research/data/parser/IndexSearcher.kt b/app/src/main/java/com/example/research/data/parser/IndexSearcher.kt index 085d755..de451eb 100644 --- a/app/src/main/java/com/example/research/data/parser/IndexSearcher.kt +++ b/app/src/main/java/com/example/research/data/parser/IndexSearcher.kt @@ -454,11 +454,7 @@ class IndexSearcher(private val context: Context) { val originalWord = readPooledString(source, source.readUnsignedShort()) val offset = ArticleOffset(source.readLong()) val length = ArticleLength(source.readInt()) - val isAlias = if (metadata.version >= 6) { - source.readUnsignedByte() and ENTRY_FLAG_ALIAS != 0 - } else { - false - } + val isAlias = metadata.version >= 6 && source.readUnsignedByte() and ENTRY_FLAG_ALIAS != 0 return IndexEntry(word, originalWord, offset, length, isAlias = isAlias) } @@ -472,8 +468,11 @@ class IndexSearcher(private val context: Context) { private fun openSource(pathOrUri: String): RandomAccessSource { val file = File(pathOrUri) - if (!file.exists()) throw DictionaryError.FileSystemError.NotFound(pathOrUri) - return FileSource(file) + return if (file.exists()) { + FileSource(file) + } else { + throw DictionaryError.FileSystemError.NotFound(pathOrUri) + } } private fun openInputStream(pathOrUri: String): java.io.InputStream { val file = File(pathOrUri) diff --git a/app/src/main/java/com/example/research/data/repository/KotlinDictionaryEngine.kt b/app/src/main/java/com/example/research/data/repository/KotlinDictionaryEngine.kt index 2ee360b..c8b1a25 100644 --- a/app/src/main/java/com/example/research/data/repository/KotlinDictionaryEngine.kt +++ b/app/src/main/java/com/example/research/data/repository/KotlinDictionaryEngine.kt @@ -112,20 +112,20 @@ class KotlinDictionaryEngine(private val context: Context) : ProgressIndexingEng maxResults: Int ): List = withContext(Dispatchers.IO) { val allResults = mutableListOf() - for (dict in dictionaries) { + for ((name, path, indexPath, _, _, metadata) in dictionaries) { try { - val indexPath = resolveIndexPath(dict.indexPath) - val entry = indexSearcher.findFirstEntry(indexPath, query) + val resolvedIndexPath = resolveIndexPath(indexPath) + val entry = indexSearcher.findFirstEntry(resolvedIndexPath, query) if (entry != null) { - allResults.add(entry.withDictionary(dict.metadata.name.ifBlank { dict.name }, dict.path)) + allResults.add(entry.withDictionary(metadata.name.ifBlank { name }, path)) } if (allResults.size >= maxResults) break } catch (e: OutOfMemoryError) { - Log.e(TAG, "search() - OOM for ${dict.indexPath}", e) + Log.e(TAG, "search() - OOM for $indexPath", e) trimMemory(80) break } catch (e: Exception) { - Log.w(TAG, "search() - error for ${dict.indexPath}: ${e.message}") + Log.w(TAG, "search() - error for $indexPath: ${e.message}") } } allResults diff --git a/app/src/main/java/com/example/research/data/repository/LocalDictionaryRepository.kt b/app/src/main/java/com/example/research/data/repository/LocalDictionaryRepository.kt index 1033d58..b376d2d 100644 --- a/app/src/main/java/com/example/research/data/repository/LocalDictionaryRepository.kt +++ b/app/src/main/java/com/example/research/data/repository/LocalDictionaryRepository.kt @@ -538,12 +538,12 @@ class LocalDictionaryRepository( val dictionaries = mutableDictionaries.value if (dictionaries.isEmpty()) return@withContext - for (dict in dictionaries) { + for ((_, _, indexPath) in dictionaries) { try { - val indexPath = resolveIndexPath(dict.indexPath) + val resolvedIndexPath = resolveIndexPath(indexPath) val indexSearcher = engine.getIndexSearcher() - if (!indexSearcher.isMetadataLoaded(indexPath)) { - engine.preloadIndexMetadata(indexPath) + if (!indexSearcher.isMetadataLoaded(resolvedIndexPath)) { + engine.preloadIndexMetadata(resolvedIndexPath) } yield() } catch (_: Exception) { /* Ignore preload failure */ } diff --git a/app/src/main/java/com/example/research/feature/download/repository/DictionaryCleaner.kt b/app/src/main/java/com/example/research/feature/download/repository/DictionaryCleaner.kt index 2a1c6d3..67bbff6 100644 --- a/app/src/main/java/com/example/research/feature/download/repository/DictionaryCleaner.kt +++ b/app/src/main/java/com/example/research/feature/download/repository/DictionaryCleaner.kt @@ -54,17 +54,17 @@ class DictionaryCleaner( for ((prefix, items) in parsedByPrefix) { val latestDsl = latestDslDateByPrefix[prefix] ?: continue val dslDates = dslDatesByPrefix[prefix].orEmpty() - for (item in items) { - if (DateUtils.isDateBefore(item.date, latestDsl)) { - toDelete.add(item.name) + for ((name, _, date, ext) in items) { + if (DateUtils.isDateBefore(date, latestDsl)) { + toDelete.add(name) continue } - if (item.ext in indexExtensions && item.date != latestDsl) { - toDelete.add(item.name) + if (ext in indexExtensions && date != latestDsl) { + toDelete.add(name) continue } - if (item.ext in indexExtensions && item.date !in dslDates) { - toDelete.add(item.name) + if (ext in indexExtensions && date !in dslDates) { + toDelete.add(name) } } } diff --git a/app/src/main/java/com/example/research/feature/download/repository/DictionaryExtractor.kt b/app/src/main/java/com/example/research/feature/download/repository/DictionaryExtractor.kt index 0547ca3..391545d 100644 --- a/app/src/main/java/com/example/research/feature/download/repository/DictionaryExtractor.kt +++ b/app/src/main/java/com/example/research/feature/download/repository/DictionaryExtractor.kt @@ -46,14 +46,13 @@ class DictionaryExtractor( reportProgress(processedIndex.toFloat() / totalFiles) } - for (file in gzFiles) { - val fileName = file.name + for ((fileName, uri) in gzFiles) { val baseName = fileName.dropLast(DictionaryConfig.GZ_EXTENSION.length) val lowerBaseName = baseName.lowercase() if (baseName.isBlank() || lowerBaseName.endsWith(DictionaryConfig.GZ_EXTENSION) || lowerBaseName.endsWith(".txt")) { - ArchiveUtils.deleteFile(context, file.uri) + ArchiveUtils.deleteFile(context, uri) markFileProcessed() continue } @@ -65,19 +64,19 @@ class DictionaryExtractor( val dslFileName = "$dslBaseName${DictionaryConfig.DSL_EXTENSION}" val existingDslFile = fileStorageManager.findFile(folderUri, dslFileName) if (existingDslFile != null && existingDslFile.exists()) { - ArchiveUtils.deleteFile(context, file.uri) + ArchiveUtils.deleteFile(context, uri) markFileProcessed() continue } val dslUri = fileStorageManager.createFileInFolder(folderUri, dslFileName, overwrite = false) ?: continue - val extractResult = ArchiveUtils.extractGzWithBom(context, file.uri, dslUri) + val extractResult = ArchiveUtils.extractGzWithBom(context, uri, dslUri) if (extractResult.isSuccess) { val extractedFile = fileStorageManager.getDocumentFile(dslUri) if (extractedFile != null && extractedFile.exists()) { if (extractedFile.length() == 0L) { ArchiveUtils.deleteFile(context, dslUri) - ArchiveUtils.deleteFile(context, file.uri) + ArchiveUtils.deleteFile(context, uri) markFileProcessed() continue } @@ -92,7 +91,7 @@ class DictionaryExtractor( ArchiveUtils.deleteFile(context, dslUri) } } - ArchiveUtils.deleteFile(context, file.uri) + ArchiveUtils.deleteFile(context, uri) } else { Log.e("DictionaryExtractor", "File extraction error: $fileName") ArchiveUtils.deleteFile(context, dslUri) @@ -100,12 +99,11 @@ class DictionaryExtractor( markFileProcessed() } - for (file in rawDslFiles) { - val dslFileName = file.name + for ((dslFileName, uri) in rawDslFiles) { val dzFileName = "$dslFileName.dz" val existingDz = fileStorageManager.findFile(folderUri, dzFileName) if (existingDz != null && existingDz.exists()) { - ArchiveUtils.deleteFile(context, file.uri) + ArchiveUtils.deleteFile(context, uri) markFileProcessed() continue } @@ -113,11 +111,11 @@ class DictionaryExtractor( val fileBaseProgress = processedIndex.toFloat() / totalFiles val fileProgressWeight = 1f / totalFiles reportProgress(fileBaseProgress) - val compressResult = ensureDslDictZip(file.uri, dslFileName, folderUri) { compressProgress -> + val compressResult = ensureDslDictZip(uri, dslFileName, folderUri) { compressProgress -> reportProgress(fileBaseProgress + (compressProgress * fileProgressWeight)) } if (compressResult.isSuccess) { - ArchiveUtils.deleteFile(context, file.uri) + ArchiveUtils.deleteFile(context, uri) } markFileProcessed() } @@ -130,11 +128,10 @@ class DictionaryExtractor( !lowerName.endsWith(DictionaryConfig.DICTZIP_EXTENSION) && !lowerName.endsWith(".dsl.gz") } - for (file in remainingRawDslFiles) { - val dslFileName = file.name - val compressResult = ensureDslDictZip(file.uri, dslFileName, folderUri, onProgress = null) + for ((dslFileName, uri) in remainingRawDslFiles) { + val compressResult = ensureDslDictZip(uri, dslFileName, folderUri, onProgress = null) if (compressResult.isSuccess) { - ArchiveUtils.deleteFile(context, file.uri) + ArchiveUtils.deleteFile(context, uri) } } @@ -152,18 +149,18 @@ class DictionaryExtractor( try { withContext(Dispatchers.IO + NonCancellable) { val files = fileStorageManager.listFilesInFolder(folderUri) - for (file in files) { - val n = file.name.lowercase() + for ((name, uri) in files) { + val n = name.lowercase() val isRawDsl = n.endsWith(DictionaryConfig.DSL_EXTENSION) && !n.endsWith(DictionaryConfig.DICTZIP_EXTENSION) && !n.endsWith(".dsl.gz") val isBareDz = n.endsWith(".dz") && !n.endsWith(DictionaryConfig.DICTZIP_EXTENSION) when { - isBareDz -> ArchiveUtils.deleteFile(context, file.uri) + isBareDz -> ArchiveUtils.deleteFile(context, uri) isRawDsl -> { - val pair = fileStorageManager.findFile(folderUri, file.name + ".dz") + val pair = fileStorageManager.findFile(folderUri, "$name.dz") if (pair == null || !pair.exists()) { - ArchiveUtils.deleteFile(context, file.uri) + ArchiveUtils.deleteFile(context, uri) } } } diff --git a/app/src/main/java/com/example/research/feature/download/repository/DictionaryRepository.kt b/app/src/main/java/com/example/research/feature/download/repository/DictionaryRepository.kt index 995c737..0b4830a 100644 --- a/app/src/main/java/com/example/research/feature/download/repository/DictionaryRepository.kt +++ b/app/src/main/java/com/example/research/feature/download/repository/DictionaryRepository.kt @@ -144,9 +144,8 @@ class DictionaryRepository( if (filesToDelete.isNotEmpty()) { var deletedCount = 0 - for (file in filesToDelete) { - val fileName = file.name - val docFile = fileStorageManager.getDocumentFile(file.uri) + for ((fileName, uri) in filesToDelete) { + val docFile = fileStorageManager.getDocumentFile(uri) if (docFile != null && docFile.exists()) { val deleted = docFile.delete() if (deleted) { diff --git a/app/src/main/java/com/example/research/ui/about/AboutLibrariesData.kt b/app/src/main/java/com/example/research/ui/about/AboutLibrariesData.kt index 9baa839..a65044a 100644 --- a/app/src/main/java/com/example/research/ui/about/AboutLibrariesData.kt +++ b/app/src/main/java/com/example/research/ui/about/AboutLibrariesData.kt @@ -2,6 +2,7 @@ package com.example.research.ui.about import android.content.Context import androidx.annotation.RawRes +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -75,6 +76,7 @@ internal object AboutLibrariesParser { ignoreUnknownKeys = true } + @OptIn(ExperimentalSerializationApi::class) fun read(context: Context, @RawRes resourceId: Int): AboutLibrariesData = context.resources.openRawResource(resourceId) .use { stream -> json.decodeFromStream(stream) } diff --git a/app/src/main/java/com/example/research/ui/about/AboutScreen.kt b/app/src/main/java/com/example/research/ui/about/AboutScreen.kt index c66363b..aacc9ff 100644 --- a/app/src/main/java/com/example/research/ui/about/AboutScreen.kt +++ b/app/src/main/java/com/example/research/ui/about/AboutScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -57,7 +56,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AboutScreen( onNavigateBack: () -> Unit @@ -182,7 +181,6 @@ private fun AboutHeader(versionName: String) { } } -@OptIn(ExperimentalLayoutApi::class) @Composable private fun AboutLibraryRow( modifier: Modifier = Modifier, diff --git a/app/src/main/java/com/example/research/ui/article/ArticleScreen.kt b/app/src/main/java/com/example/research/ui/article/ArticleScreen.kt index e3342ad..479fe01 100644 --- a/app/src/main/java/com/example/research/ui/article/ArticleScreen.kt +++ b/app/src/main/java/com/example/research/ui/article/ArticleScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -349,7 +348,6 @@ fun ArticleTitleBar( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun ArticleScreen( articleState: ArticleState?, diff --git a/app/src/main/java/com/example/research/ui/main/MainScreen.kt b/app/src/main/java/com/example/research/ui/main/MainScreen.kt index 47704de..c413a3f 100644 --- a/app/src/main/java/com/example/research/ui/main/MainScreen.kt +++ b/app/src/main/java/com/example/research/ui/main/MainScreen.kt @@ -73,13 +73,9 @@ fun MainRoute( val showDictionaryName = remember(settingsState) { val activeDictionaries = settingsState.dictionaries.filter { it.isActive } - if (activeDictionaries.isEmpty()) { - false - } else { - activeDictionaries.any { dict -> - val fileName = dict.path.substringAfterLast("/") - !DictionaryConfig.isDabkrsDictionary(fileName) - } + activeDictionaries.any { dict -> + val fileName = dict.path.substringAfterLast("/") + !DictionaryConfig.isDabkrsDictionary(fileName) } } diff --git a/app/src/main/java/com/example/research/ui/main/components/MainTopBar.kt b/app/src/main/java/com/example/research/ui/main/components/MainTopBar.kt index 0d9354d..2f642db 100644 --- a/app/src/main/java/com/example/research/ui/main/components/MainTopBar.kt +++ b/app/src/main/java/com/example/research/ui/main/components/MainTopBar.kt @@ -1,23 +1,17 @@ package com.example.research.ui.main.components -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions @@ -60,7 +54,6 @@ import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.example.research.R import com.example.research.common.ui.appTestTag @@ -73,6 +66,7 @@ import com.example.research.ui.theme.Spacing import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.milliseconds private val TopBarHeight = Spacing.topBarHeight private val TopBarShape = RoundedCornerShape(28.dp) @@ -145,7 +139,7 @@ fun MainTopBar( onShowHistory = { focusManager.clearFocus(force = true) coroutineScope.launch { - withTimeoutOrNull(350) { + withTimeoutOrNull(350.milliseconds) { snapshotFlow { imeInsets.getBottom(density) > 0 } .first { visible -> !visible } } @@ -196,7 +190,6 @@ fun MainTopBar( } @Composable -@OptIn(ExperimentalMaterial3Api::class) private fun SearchField( searchState: TextFieldState, enabled: Boolean, @@ -298,20 +291,17 @@ private fun SearchHistorySheet( ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val density = LocalDensity.current - val configuration = LocalConfiguration.current - var frozenHeight by remember { mutableStateOf(null) } - if (frozenHeight == null) { - frozenHeight = if (isSearchBarAtBottom) { - configuration.screenHeightDp.dp - - HistorySheetChrome - - with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } - } else { - with(density) { - (configuration.screenHeightDp.dp.toPx() - topBarBottomPx).toDp() + val screenHeight = LocalConfiguration.current.screenHeightDp.dp + val navigationBarsBottom = WindowInsets.navigationBars.getBottom(density) + val maxContentHeight = remember { + with(density) { + if (isSearchBarAtBottom) { + screenHeight - HistorySheetChrome - navigationBarsBottom.toDp() + } else { + screenHeight - topBarBottomPx.toDp() } } } - val maxContentHeight = frozenHeight!! ModalBottomSheet( onDismissRequest = onDismiss, diff --git a/app/src/main/java/com/example/research/ui/navigation/AppNavigation.kt b/app/src/main/java/com/example/research/ui/navigation/AppNavigation.kt index a7ca9fc..49ea172 100644 --- a/app/src/main/java/com/example/research/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/example/research/ui/navigation/AppNavigation.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 import androidx.compose.runtime.* import androidx.compose.runtime.saveable.* @@ -39,7 +38,6 @@ enum class Screen { Home, Settings, Article, About } -@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable fun AppNavigation( searchViewModel: SearchViewModel, diff --git a/app/src/main/java/com/example/research/ui/settings/SettingsScreen.kt b/app/src/main/java/com/example/research/ui/settings/SettingsScreen.kt index 720e3cb..1a5994c 100644 --- a/app/src/main/java/com/example/research/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/example/research/ui/settings/SettingsScreen.kt @@ -57,7 +57,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsRoute( viewModel: SettingsViewModel, diff --git a/app/src/main/java/com/example/research/ui/theme/Spacing.kt b/app/src/main/java/com/example/research/ui/theme/Spacing.kt index baf6172..0da25a0 100644 --- a/app/src/main/java/com/example/research/ui/theme/Spacing.kt +++ b/app/src/main/java/com/example/research/ui/theme/Spacing.kt @@ -6,7 +6,6 @@ object Spacing { // Semantic spacing constants for specific UI elements val topBarPadding = 8.dp val topBarHeight = 56.dp - val searchTextStart = 68.dp val listItemVertical = 8.dp val contentStart = 32.dp