Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/org/fossify/documents/helpers/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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),
Expand All @@ -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
}
}
}
Expand Down Expand Up @@ -122,15 +119,16 @@ private fun TextFieldValue.toggleNumberedList(): TextFieldValue {
private fun TextFieldValue.transformSelectedLines(
transform: (List<String>) -> List<String>,
): 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)
Expand All @@ -140,31 +138,18 @@ 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 {
wrapSelection("`", "`", "code")
}
}

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+")
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Set<DocumentFilter>>(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<List<DocumentFolder>>(emptyList())
private val folderRefresh = MutableStateFlow(0L)
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,4 @@ class TextDocumentDisplayTest {
fun `blank query has no matches`() {
assertEquals(emptyList<TextRange>(), 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))
}
}
Loading