From 2927324cf81396bf4186087144528c2e2a202112 Mon Sep 17 00:00:00 2001 From: Naveen Singh Date: Fri, 23 Oct 2026 05:03:20 +0530 Subject: [PATCH] fix: persist list/grid view preference to config --- CHANGELOG.md | 3 + .../org/fossify/documents/helpers/Config.kt | 8 +++ .../ui/screens/MarkdownEditAction.kt | 61 +++++++------------ .../ui/theme/DocumentsAppThemeSurface.kt | 60 +----------------- .../viewmodels/DocumentsViewModel.kt | 11 +++- .../ui/screens/TextDocumentDisplayTest.kt | 13 ---- 6 files changed, 44 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb18551a..28ba4685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Fixed list/grid view resetting when reopening the app + ## [1.1.0] - 2026-10-10 ### Added - PDF text selection and copying diff --git a/app/src/main/java/org/fossify/documents/helpers/Config.kt b/app/src/main/java/org/fossify/documents/helpers/Config.kt index 0b6747cf..7e19ed2a 100644 --- a/app/src/main/java/org/fossify/documents/helpers/Config.kt +++ b/app/src/main/java/org/fossify/documents/helpers/Config.kt @@ -3,11 +3,13 @@ package org.fossify.documents.helpers import android.content.Context import androidx.core.content.edit import org.fossify.commons.helpers.BaseConfig +import org.fossify.documents.models.DocumentViewMode class Config(context: Context) : BaseConfig(context) { companion object { fun newInstance(context: Context) = Config(context) + private const val DOCUMENT_VIEW_MODE = "document_view_mode" private const val DOCUMENTS = "documents" private const val DOCUMENT_FOLDERS = "document_folders" private const val EDITOR_TEXT_ZOOM = "editor_text_zoom" @@ -18,6 +20,12 @@ class Config(context: Context) : BaseConfig(context) { private const val SHOW_FILE_LOCATIONS = "show_file_locations" } + var documentViewMode: DocumentViewMode + get() = DocumentViewMode.entries.firstOrNull { + it.name == prefs.getString(DOCUMENT_VIEW_MODE, null) + } ?: DocumentViewMode.LIST + set(value) = prefs.edit { putString(DOCUMENT_VIEW_MODE, value.name) } + var documentsJson: String get() = prefs.getString(DOCUMENTS, "[]")!! set(value) = prefs.edit { diff --git a/app/src/main/java/org/fossify/documents/ui/screens/MarkdownEditAction.kt b/app/src/main/java/org/fossify/documents/ui/screens/MarkdownEditAction.kt index 81cdfb2a..86e90179 100644 --- a/app/src/main/java/org/fossify/documents/ui/screens/MarkdownEditAction.kt +++ b/app/src/main/java/org/fossify/documents/ui/screens/MarkdownEditAction.kt @@ -14,7 +14,7 @@ internal enum class MarkdownEditAction { Code, } -internal fun TextFieldValue.applyMarkdownAction(action: MarkdownEditAction): TextFieldValue { +private fun TextFieldValue.applyMarkdownAction(action: MarkdownEditAction): TextFieldValue { return when (action) { MarkdownEditAction.Heading -> toggleHeading() MarkdownEditAction.Bold -> wrapSelection("**", "**", "bold") @@ -43,8 +43,9 @@ private fun TextFieldValue.wrapSelection( suffix: String, placeholder: String, ): TextFieldValue { - val range = selection.normalizedBounds() - val selectedText = text.substring(range.start, range.end) + val start = selection.min + val end = selection.max + val selectedText = text.substring(start, end) val selectionCanContainMarkers = selectedText.length >= prefix.length + suffix.length val selectionStartsWithPrefix = selectedText.startsWith(prefix) val selectionEndsWithSuffix = selectedText.endsWith(suffix) @@ -53,15 +54,15 @@ private fun TextFieldValue.wrapSelection( .removePrefix(prefix) .removeSuffix(suffix) return TextFieldValue( - text = text.replaceRange(range.start, range.end, replacement), - selection = TextRange(range.start, range.start + replacement.length), + text = text.replaceRange(start, end, replacement), + selection = TextRange(start, start + replacement.length), ) } - val outerStart = range.start - prefix.length - val outerEnd = range.end + suffix.length + val outerStart = start - prefix.length + val outerEnd = end + suffix.length val hasOuterPrefix = outerStart >= 0 && text.regionMatches(outerStart, prefix, 0, prefix.length) - val hasOuterSuffix = outerEnd <= text.length && text.regionMatches(range.end, suffix, 0, suffix.length) + val hasOuterSuffix = outerEnd <= text.length && text.regionMatches(end, suffix, 0, suffix.length) if (hasOuterPrefix && hasOuterSuffix) { return TextFieldValue( text = text.replaceRange(outerStart, outerEnd, selectedText), @@ -71,27 +72,23 @@ private fun TextFieldValue.wrapSelection( val content = selectedText.ifEmpty { placeholder } val replacement = prefix + content + suffix - val newText = text.replaceRange(range.start, range.end, replacement) + val newText = text.replaceRange(start, end, replacement) val newSelection = if (selectedText.isEmpty()) { - TextRange(range.start + prefix.length, range.start + prefix.length + placeholder.length) + TextRange(start + prefix.length, start + prefix.length + placeholder.length) } else { - TextRange(range.start, range.start + replacement.length) + TextRange(start, start + replacement.length) } return TextFieldValue(newText, selection = newSelection) } -private fun TextFieldValue.prefixSelectedLines( - prefix: String, - transformLine: (String) -> String = { it }, -): TextFieldValue { +private fun TextFieldValue.prefixSelectedLines(prefix: String): TextFieldValue { return transformSelectedLines { lines -> lines.map { line -> - val transformed = transformLine(line) - if (transformed.startsWith(prefix)) { - transformed.removePrefix(prefix) + if (line.startsWith(prefix)) { + line.removePrefix(prefix) } else { - prefix + transformed + prefix + line } } } @@ -122,15 +119,16 @@ private fun TextFieldValue.toggleNumberedList(): TextFieldValue { private fun TextFieldValue.transformSelectedLines( transform: (List) -> List, ): TextFieldValue { - val range = selection.normalizedBounds() - val lineStart = if (range.start == 0) { + val start = selection.min + val end = selection.max + val lineStart = if (start == 0) { 0 } else { - text.lastIndexOf('\n', range.start - 1).let { index -> + text.lastIndexOf('\n', start - 1).let { index -> if (index == -1) 0 else index + 1 } } - val lineEnd = text.indexOf('\n', range.end).let { index -> + val lineEnd = text.indexOf('\n', end).let { index -> if (index == -1) text.length else index } val selectedLines = text.substring(lineStart, lineEnd) @@ -140,13 +138,12 @@ private fun TextFieldValue.transformSelectedLines( return TextFieldValue( text = newText, - selection = TextRange((range.end + delta).coerceIn(0, newText.length)), + selection = TextRange((end + delta).coerceIn(0, newText.length)), ) } private fun TextFieldValue.codeSelection(): TextFieldValue { - val range = selection.normalizedBounds() - val selectedText = text.substring(range.start, range.end) + val selectedText = text.substring(selection.min, selection.max) return if ('\n' in selectedText) { wrapSelection("```\n", "\n```", selectedText) } else { @@ -154,17 +151,5 @@ private fun TextFieldValue.codeSelection(): TextFieldValue { } } -private fun TextRange.normalizedBounds(): SelectionBounds { - return SelectionBounds( - start = minOf(start, end), - end = maxOf(start, end), - ) -} - -private data class SelectionBounds( - val start: Int, - val end: Int, -) - private val headingPrefixRegex = Regex("^#{1,6}\\s*") private val numberedListPrefixRegex = Regex("^\\d+[.)]\\s+") diff --git a/app/src/main/java/org/fossify/documents/ui/theme/DocumentsAppThemeSurface.kt b/app/src/main/java/org/fossify/documents/ui/theme/DocumentsAppThemeSurface.kt index 52564a2c..c28fd04b 100644 --- a/app/src/main/java/org/fossify/documents/ui/theme/DocumentsAppThemeSurface.kt +++ b/app/src/main/java/org/fossify/documents/ui/theme/DocumentsAppThemeSurface.kt @@ -1,79 +1,23 @@ package org.fossify.documents.ui.theme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily import org.fossify.commons.compose.system_ui_controller.rememberSystemUiController import org.fossify.commons.compose.theme.AppThemeSurface import org.fossify.commons.compose.theme.SimpleTheme -import org.fossify.commons.extensions.baseConfig -import org.fossify.commons.helpers.FONT_TYPE_CUSTOM -import org.fossify.commons.helpers.FontHelper -import java.io.File @Composable internal fun DocumentsAppThemeSurface( content: @Composable () -> Unit, ) { AppThemeSurface { - MaterialTheme( - colorScheme = SimpleTheme.colorScheme, - typography = documentsTypography(), - shapes = SimpleTheme.shapes, - ) { - DocumentsSystemBars() - content() - } + DocumentsSystemBars() + content() } } -@Composable -private fun documentsTypography(): Typography { - val context = LocalContext.current - val baseTypography = SimpleTheme.typography - val config = context.baseConfig - if (config.fontType != FONT_TYPE_CUSTOM) { - return baseTypography - } - - val fontFile = File(FontHelper.getFontsDir(context), config.fontName) - if (!fontFile.exists()) { - return baseTypography - } - - val fontFamily = remember(fontFile.path, fontFile.lastModified()) { - FontFamily(Font(fontFile)) - } - return remember(baseTypography, fontFamily) { - baseTypography.withFontFamily(fontFamily) - } -} - -private fun Typography.withFontFamily(fontFamily: FontFamily) = Typography( - displayLarge = displayLarge.copy(fontFamily = fontFamily), - displayMedium = displayMedium.copy(fontFamily = fontFamily), - displaySmall = displaySmall.copy(fontFamily = fontFamily), - headlineLarge = headlineLarge.copy(fontFamily = fontFamily), - headlineMedium = headlineMedium.copy(fontFamily = fontFamily), - headlineSmall = headlineSmall.copy(fontFamily = fontFamily), - titleLarge = titleLarge.copy(fontFamily = fontFamily), - titleMedium = titleMedium.copy(fontFamily = fontFamily), - titleSmall = titleSmall.copy(fontFamily = fontFamily), - bodyLarge = bodyLarge.copy(fontFamily = fontFamily), - bodyMedium = bodyMedium.copy(fontFamily = fontFamily), - bodySmall = bodySmall.copy(fontFamily = fontFamily), - labelLarge = labelLarge.copy(fontFamily = fontFamily), - labelMedium = labelMedium.copy(fontFamily = fontFamily), - labelSmall = labelSmall.copy(fontFamily = fontFamily), -) - @Composable private fun DocumentsSystemBars() { val controller = rememberSystemUiController() diff --git a/app/src/main/java/org/fossify/documents/viewmodels/DocumentsViewModel.kt b/app/src/main/java/org/fossify/documents/viewmodels/DocumentsViewModel.kt index 97eec5d2..a675771e 100644 --- a/app/src/main/java/org/fossify/documents/viewmodels/DocumentsViewModel.kt +++ b/app/src/main/java/org/fossify/documents/viewmodels/DocumentsViewModel.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.withContext import org.fossify.documents.R import org.fossify.documents.data.DocumentFolderContent import org.fossify.documents.data.DocumentsRepository +import org.fossify.documents.helpers.Config import org.fossify.documents.models.DocumentEntry import org.fossify.documents.models.DocumentFilter import org.fossify.documents.models.DocumentFolder @@ -40,10 +41,11 @@ class DocumentsViewModel( application: Application, ) : AndroidViewModel(application) { private val repository = DocumentsRepository(application) + private val config = Config.newInstance(application) private val query = MutableStateFlow("") private val selectedFilters = MutableStateFlow>(emptySet()) private val selectedSort = MutableStateFlow(DocumentSort.RECENT) - private val selectedViewMode = MutableStateFlow(DocumentViewMode.LIST) + private val selectedViewMode = MutableStateFlow(config.documentViewMode) private val homeSection = MutableStateFlow(DocumentsHomeSection.HOME) private val folderPath = MutableStateFlow>(emptyList()) private val folderRefresh = MutableStateFlow(0L) @@ -229,7 +231,7 @@ class DocumentsViewModel( }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(UI_STATE_STOP_TIMEOUT_MS), - initialValue = DocumentsUiState() + initialValue = DocumentsUiState(selectedViewMode = config.documentViewMode) ) fun onFilterSelected(filter: DocumentFilter) { @@ -245,7 +247,10 @@ class DocumentsViewModel( viewMode: DocumentViewMode? = null, ) { sort?.let { selectedSort.value = it } - viewMode?.let { selectedViewMode.value = it } + viewMode?.let { + config.documentViewMode = it + selectedViewMode.value = it + } } fun showSection(section: DocumentsHomeSection, folder: DocumentFolder? = null) { diff --git a/app/src/test/java/org/fossify/documents/ui/screens/TextDocumentDisplayTest.kt b/app/src/test/java/org/fossify/documents/ui/screens/TextDocumentDisplayTest.kt index d472fdcb..9ab2a4a8 100644 --- a/app/src/test/java/org/fossify/documents/ui/screens/TextDocumentDisplayTest.kt +++ b/app/src/test/java/org/fossify/documents/ui/screens/TextDocumentDisplayTest.kt @@ -33,17 +33,4 @@ class TextDocumentDisplayTest { fun `blank query has no matches`() { assertEquals(emptyList(), findTextMatches("document", "")) } - - @Test - fun `clamps zoom to the supported range`() { - assertEquals(MIN_DOCUMENT_TEXT_ZOOM, 0.1f.coerceDocumentTextZoom()) - assertEquals(MAX_DOCUMENT_TEXT_ZOOM, 10f.coerceDocumentTextZoom()) - } - - @Test - fun `skips highlighting for excessive match counts`() { - assertEquals(true, shouldHighlightTextMatches(matchCount = 120)) - assertEquals(false, shouldHighlightTextMatches(matchCount = 6_000)) - assertEquals(false, shouldHighlightTextMatches(matchCount = 0)) - } }