Skip to content

Commit

Permalink
Allow searching in documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Waboodoo committed Dec 21, 2023
1 parent 0a8eb53 commit 6cfae68
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 12 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

### Improvements
- When using multiple variables in a shortcut, their values are now resolved in deterministic order, according to the order in which the variables appear on the Variables screen
- Built-in icons are now treated as adaptive icons, allowing them to look nicer on the home screen of devices that support this
- Built-in icons are now treated as adaptive icons, allowing them to have nicer backgrounds when placed on the home screen of devices that support this
- It is now possible to search for text in the built-in documentation pages

### Miscellaneous
- When an HTML response tries to open a URL, either because of a redirect or a clicked link, it will first warn about the use of an external browser
Expand Down
2 changes: 1 addition & 1 deletion HTTPShortcuts/app/src/main/assets/changelog.html

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.viewinterop.NoOpUpdate
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ch.rmy.android.http_shortcuts.activities.documentation.models.SearchDirection
import ch.rmy.android.http_shortcuts.extensions.rememberWebView
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlin.time.Duration.Companion.milliseconds

@SuppressLint("SetJavaScriptEnabled")
@Composable
fun DocumentationBrowser(
url: Uri,
searchQuery: String?,
searchDirectionRequests: Flow<SearchDirection>,
onPageChanged: (Uri) -> Unit,
onPageTitle: (String?) -> Unit,
onLoadingStateChanged: (loading: Boolean) -> Unit,
onExternalUrl: (Uri) -> Unit,
onSearchResults: (Int, Int) -> Unit,
modifier: Modifier = Modifier,
) {
val webView = rememberWebView(key = "documentation") { context, _ ->
Expand All @@ -39,6 +46,27 @@ fun DocumentationBrowser(
webView.loadUrl(internalUrl)
}
}
LaunchedEffect(webView) {
webView.setFindListener { activeMatchOrdinal, numberOfMatches, _ ->
onSearchResults(activeMatchOrdinal + if (numberOfMatches > 0) 1 else 0, numberOfMatches)
}
}
LaunchedEffect(searchQuery) {
if (searchQuery != null) {
delay(200.milliseconds)
webView.findAllAsync(searchQuery)
} else {
webView.findAllAsync("")
}
}
LaunchedEffect(searchDirectionRequests) {
searchDirectionRequests.collect {
when (it) {
SearchDirection.PREVIOUS -> webView.findNext(false)
SearchDirection.NEXT -> webView.findNext(true)
}
}
}

AndroidView(
modifier = modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,21 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import ch.rmy.android.http_shortcuts.activities.documentation.models.SearchDirection
import ch.rmy.android.http_shortcuts.components.LoadingIndicator
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlin.time.Duration.Companion.milliseconds

@Composable
fun DocumentationContent(
url: Uri,
searchQuery: String?,
searchDirectionRequests: Flow<SearchDirection>,
onPageChanged: (Uri) -> Unit,
onPageTitle: (String?) -> Unit,
onExternalUrl: (Uri) -> Unit,
onSearchResults: (Int, Int) -> Unit,
) {
var isLoading by remember {
mutableStateOf(true)
Expand All @@ -46,11 +51,14 @@ fun DocumentationContent(
}

DocumentationBrowser(
url,
onPageChanged,
onPageTitle,
url = url,
searchQuery = searchQuery,
searchDirectionRequests = searchDirectionRequests,
onPageChanged = onPageChanged,
onPageTitle = onPageTitle,
onLoadingStateChanged = { isLoading = it },
onExternalUrl,
onExternalUrl = onExternalUrl,
onSearchResults = onSearchResults,
modifier = Modifier
.fillMaxSize()
.alpha(if (isLoadingScreenVisible) 0f else 1f),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,57 @@
package ch.rmy.android.http_shortcuts.activities.documentation

import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.OpenInBrowser
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.outlined.KeyboardArrowDown
import androidx.compose.material.icons.outlined.KeyboardArrowUp
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ch.rmy.android.framework.extensions.consume
import ch.rmy.android.framework.extensions.openURL
import ch.rmy.android.http_shortcuts.R
import ch.rmy.android.http_shortcuts.activities.documentation.models.SearchDirection
import ch.rmy.android.http_shortcuts.components.EventHandler
import ch.rmy.android.http_shortcuts.components.FontSize
import ch.rmy.android.http_shortcuts.components.SimpleScaffold
import ch.rmy.android.http_shortcuts.components.Spacing
import ch.rmy.android.http_shortcuts.components.ToolbarIcon
import ch.rmy.android.http_shortcuts.components.bindViewModel
import ch.rmy.android.http_shortcuts.extensions.runIf
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch

@Composable
fun DocumentationScreen(url: Uri?) {
Expand All @@ -25,6 +60,7 @@ fun DocumentationScreen(url: Uri?) {
)

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
EventHandler { event ->
when (event) {
is DocumentationEvent.OpenInBrowser -> consume {
Expand All @@ -37,24 +73,162 @@ fun DocumentationScreen(url: Uri?) {
var subtitle by remember {
mutableStateOf<String?>(null)
}
var searchQuery by remember {
mutableStateOf<String?>(null)
}
var searchResults by remember {
mutableStateOf<Pair<Int, Int>?>(null)
}
val searchDirectionRequests = remember {
MutableSharedFlow<SearchDirection>()
}

BackHandler(searchQuery != null) {
searchQuery = null
}

SimpleScaffold(
viewState = state,
title = stringResource(R.string.title_documentation),
subtitle = subtitle,
actions = {
ToolbarIcon(
Icons.Filled.Search,
contentDescription = stringResource(R.string.menu_action_search),
onClick = {
searchQuery = if (searchQuery == null) "" else null
},
)
ToolbarIcon(
Icons.Filled.OpenInBrowser,
contentDescription = stringResource(R.string.button_open_documentation_in_browser),
onClick = viewModel::onOpenInBrowserButtonClicked,
)
},
) { viewState ->
DocumentationContent(
url = viewState.url,
onPageChanged = viewModel::onPageChanged,
onPageTitle = { subtitle = it },
onExternalUrl = viewModel::onExternalUrl,
Box {
DocumentationContent(
url = viewState.url,
searchQuery = searchQuery,
searchDirectionRequests = searchDirectionRequests,
onPageChanged = {
searchQuery = null
viewModel.onPageChanged(it)
},
onPageTitle = { subtitle = it },
onExternalUrl = viewModel::onExternalUrl,
onSearchResults = { current, total ->
searchResults = Pair(current, total)
}
)

searchQuery?.let {
SearchBar(
query = it,
results = searchResults,
onQueryChanged = { newQuery ->
searchQuery = newQuery
},
onNext = { direction ->
coroutineScope.launch {
searchDirectionRequests.emit(direction)
}
},
)
}
}
}
}

@Composable
private fun SearchBar(
modifier: Modifier = Modifier,
query: String,
results: Pair<Int, Int>?,
onQueryChanged: (String?) -> Unit,
onNext: (SearchDirection) -> Unit,
) {
val focusRequester = remember {
FocusRequester()
}

Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(Spacing.TINY)
.shadow(elevation = 1.dp)
.then(modifier),
horizontalArrangement = Arrangement.spacedBy(Spacing.SMALL),
verticalAlignment = Alignment.CenterVertically,
) {
TextField(
value = query,
onValueChange = onQueryChanged,
singleLine = true,
modifier = Modifier
.weight(1f)
.focusRequester(focusRequester),
placeholder = {
Text(text = stringResource(R.string.placeholder_documentation_search_query))
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
keyboardActions = KeyboardActions {
onQueryChanged(null)
}
)

results?.takeIf { query.isNotEmpty() }?.let { (current, total) ->
Text(
text = "$current/$total",
maxLines = 1,
fontSize = FontSize.SMALL,
)
}

Row(modifier = Modifier.padding(end = Spacing.SMALL)) {
val enabled = results?.second?.let { it > 1 } == true
Icon(
Icons.Outlined.KeyboardArrowUp,
null,
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
enabled = enabled,
onClick = {
onNext(SearchDirection.PREVIOUS)
},
)
.runIf(!enabled) {
alpha(0.3f)
},
)
Icon(
Icons.Outlined.KeyboardArrowDown,
null,
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
enabled = enabled,
onClick = {
onNext(SearchDirection.NEXT)
},
)
.runIf(!enabled) {
alpha(0.3f)
},
)
}

LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ch.rmy.android.http_shortcuts.activities.documentation.models

enum class SearchDirection {
PREVIOUS,
NEXT,
}
4 changes: 3 additions & 1 deletion HTTPShortcuts/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1275,7 +1275,7 @@
<string name="button_sort_variables">Sort</string>
<!-- Message: Shown in a snackbar after all variables have been sorted (alphabetically) -->
<string name="message_variables_sorted">Variables sorted.</string>
<!-- Button label on menu, for searching. Currently only used in Code Snippet picker but might be used to search in other places -->
<!-- Button label on menu, for searching. Used in Code Snippet picker and for Documentation screens -->
<string name="menu_action_search">Search</string>
<!-- Generic message for empty state when a search yields no results, displayed in the middle of the screen -->
<string name="instructions_search_no_results">No results</string>
Expand Down Expand Up @@ -1525,4 +1525,6 @@
<string name="icon_shape_round">Round</string>
<!-- Warning message shown to user when an HTML page wants to open a URL in the external browser. %s is a placeholder for the URL. -->
<string name="warning_page_wants_to_open_url">The page wants to open the following URL in a browser:\n\n%s</string>
<!-- Placeholder text shown in the text field for the search feature on the Documentation screen -->
<string name="placeholder_documentation_search_query">Search in page…</string>
</resources>

0 comments on commit 6cfae68

Please sign in to comment.