Text lookup upgrade (#66)

* Added `TooltipIconButton` and integrated tooltips for tool icons in app bars

* Added external translate and search support for EPUB and PDF readers

* docs: refine project overview and update build instructions in readme
This commit is contained in:
Aryan 2026-03-14 13:01:46 +05:30 committed by GitHub
parent 74e2cec415
commit f2c5ae25d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1172 additions and 506 deletions

View file

@ -17,12 +17,19 @@
*
* mail: epistemereader@gmail.com
*/
@file:kotlin.OptIn(ExperimentalMaterial3Api::class)
package com.aryan.reader
import android.content.Context
import androidx.annotation.OptIn
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.RichTooltip
import androidx.compose.material3.rememberTooltipState
import androidx.compose.material3.TooltipDefaults
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -105,6 +112,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
@ -242,6 +250,90 @@ fun rememberSearchState(
}
}
private val activeTooltipState = mutableStateOf<androidx.compose.material3.TooltipState?>(null)
@Composable
fun TooltipIconButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
description: String? = null,
content: @Composable () -> Unit
) {
val tooltipState = rememberTooltipState(isPersistent = true)
val scope = rememberCoroutineScope()
LaunchedEffect(tooltipState.isVisible) {
if (tooltipState.isVisible) {
val previous = activeTooltipState.value
if (previous != null && previous !== tooltipState) {
previous.dismiss()
}
activeTooltipState.value = tooltipState
} else {
if (activeTooltipState.value === tooltipState) {
activeTooltipState.value = null
}
}
}
TooltipBox(
positionProvider = if (description != null)
TooltipDefaults.rememberRichTooltipPositionProvider()
else
TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
if (description != null) {
RichTooltip(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
content()
Text(
text = text,
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
}
},
colors = TooltipDefaults.richTooltipColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
titleContentColor = MaterialTheme.colorScheme.onSurface
)
) {
Text(
text = description,
style = MaterialTheme.typography.bodySmall
)
}
} else {
PlainTooltip {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
content()
Text(text)
}
}
}
},
state = tooltipState
) {
IconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
content()
}
}
}
@Composable
fun SearchTopBar(
searchState: SearchState,
@ -265,7 +357,11 @@ fun SearchTopBar(
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onCloseSearch) {
TooltipIconButton(
text = stringResource(R.string.tooltip_close_search),
description = stringResource(R.string.tooltip_close_search_desc),
onClick = onCloseSearch
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Close Search"
@ -297,7 +393,11 @@ fun SearchTopBar(
)
if (searchState.searchQuery.isNotEmpty()) {
IconButton(onClick = { searchState.onQueryChange("") }) {
TooltipIconButton(
text = stringResource(R.string.tooltip_clear_search),
description = stringResource(R.string.tooltip_clear_search_desc),
onClick = { searchState.onQueryChange("") }
) {
Icon(
Icons.Default.Close,
contentDescription = "Clear Search"
@ -305,10 +405,20 @@ fun SearchTopBar(
}
}
IconButton(onClick = {
searchState.showSearchResultsPanel = !searchState.showSearchResultsPanel
focusManager.clearFocus()
}) {
TooltipIconButton(
text = if (searchState.showSearchResultsPanel)
stringResource(R.string.tooltip_hide_results)
else
stringResource(R.string.tooltip_show_results),
description = if (searchState.showSearchResultsPanel)
stringResource(R.string.tooltip_hide_results_desc)
else
stringResource(R.string.tooltip_show_results_desc),
onClick = {
searchState.showSearchResultsPanel = !searchState.showSearchResultsPanel
focusManager.clearFocus()
}
) {
Icon(
imageVector = if (searchState.showSearchResultsPanel) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown,
contentDescription = if (searchState.showSearchResultsPanel) "Hide Results" else "Show Results"
@ -333,7 +443,9 @@ fun SearchNavigationControls(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 4.dp)
) {
IconButton(
TooltipIconButton(
text = stringResource(R.string.tooltip_prev_result),
description = stringResource(R.string.tooltip_prev_result_desc),
onClick = { onNavigate(searchState.currentSearchResultIndex - 1) },
enabled = searchState.currentSearchResultIndex > 0
) {
@ -346,7 +458,9 @@ fun SearchNavigationControls(
modifier = Modifier.padding(horizontal = 4.dp)
)
IconButton(
TooltipIconButton(
text = stringResource(R.string.tooltip_next_result),
description = stringResource(R.string.tooltip_next_result_desc),
onClick = { onNavigate(searchState.currentSearchResultIndex + 1) },
enabled = searchState.currentSearchResultIndex < searchState.searchResultsCount - 1
) {
@ -1020,7 +1134,6 @@ suspend fun fetchRecap(
}
@OptIn(UnstableApi::class)
@kotlin.OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TtsSettingsSheet(
isVisible: Boolean,