From 30d60d51b792818f333a5b96747504451b0a68e9 Mon Sep 17 00:00:00 2001 From: Dima Date: Wed, 2 Oct 2024 20:42:20 +0900 Subject: [PATCH] PDF search. Upping compose compile version to 1.5.7 Upping kotlin gradle plugin. --- .../android/architecture/ScreenArguments.kt | 4 +- .../android/pdf/reader/PdfReaderModes.kt | 30 ++++ .../android/pdf/reader/PdfReaderScreen.kt | 4 + .../android/pdf/reader/PdfReaderTopBar.kt | 34 +++- .../pdf/reader/PdfReaderVMInterface.kt | 2 + .../android/pdf/reader/PdfReaderViewModel.kt | 38 +++- .../reader/pdfsearch/PdfReaderSearchPopup.kt | 78 +++++++++ .../reader/pdfsearch/PdfReaderSearchScreen.kt | 165 ++++++++++++++++++ .../pdfsearch/PdfReaderSearchViewModel.kt | 153 ++++++++++++++++ .../pdfsearch/data/PdfReaderSearchArgs.kt | 9 + .../pdfsearch/data/PdfReaderSearchItem.kt | 5 + .../data/PdfReaderSearchResultSelected.kt | 5 + build.gradle.kts | 4 +- buildSrc/build.gradle.kts | 4 +- buildSrc/src/main/kotlin/Libs.kt | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 16 files changed, 528 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchPopup.kt create mode 100644 app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchScreen.kt create mode 100644 app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchViewModel.kt create mode 100644 app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchArgs.kt create mode 100644 app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchItem.kt create mode 100644 app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchResultSelected.kt diff --git a/app/src/main/java/org/zotero/android/architecture/ScreenArguments.kt b/app/src/main/java/org/zotero/android/architecture/ScreenArguments.kt index 5efe7ba9..c0a9b247 100644 --- a/app/src/main/java/org/zotero/android/architecture/ScreenArguments.kt +++ b/app/src/main/java/org/zotero/android/architecture/ScreenArguments.kt @@ -4,6 +4,7 @@ import org.zotero.android.pdf.annotation.data.PdfAnnotationArgs import org.zotero.android.pdf.annotationmore.data.PdfAnnotationMoreArgs import org.zotero.android.pdf.annotationmore.editpage.data.PdfAnnotationEditPageArgs import org.zotero.android.pdf.colorpicker.data.PdfReaderColorPickerArgs +import org.zotero.android.pdf.reader.pdfsearch.data.PdfReaderSearchArgs import org.zotero.android.pdf.settings.data.PdfSettingsArgs import org.zotero.android.pdffilter.data.PdfFilterArgs import org.zotero.android.screens.addnote.data.AddOrEditNoteArgs @@ -19,7 +20,6 @@ import org.zotero.android.screens.mediaviewer.video.VideoPlayerArgs import org.zotero.android.screens.share.sharecollectionpicker.data.ShareCollectionPickerArgs import org.zotero.android.screens.sortpicker.data.SortPickerArgs import org.zotero.android.screens.tagpicker.data.TagPickerArgs -import org.zotero.android.uicomponents.addbyidentifier.data.AddByIdentifierPickerArgs import org.zotero.android.uicomponents.singlepicker.SinglePickerArgs object ScreenArguments { @@ -43,5 +43,5 @@ object ScreenArguments { lateinit var pdfAnnotationEditPageArgs: PdfAnnotationEditPageArgs lateinit var pdfReaderColorPickerArgs: PdfReaderColorPickerArgs lateinit var shareCollectionPickerArgs: ShareCollectionPickerArgs - lateinit var addByIdentifierPickerArgs: AddByIdentifierPickerArgs + lateinit var pdfReaderSearchArgs: PdfReaderSearchArgs } diff --git a/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderModes.kt b/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderModes.kt index 89be8fb2..97cc53c4 100644 --- a/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderModes.kt +++ b/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderModes.kt @@ -7,7 +7,9 @@ import androidx.compose.animation.ContentTransform import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.tween import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically import androidx.compose.animation.with import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -23,6 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import org.zotero.android.architecture.ui.CustomLayoutSize +import org.zotero.android.pdf.reader.pdfsearch.PdfReaderSearchScreen import org.zotero.android.pdf.reader.sidebar.PdfReaderSidebar import org.zotero.android.pdf.reader.sidebar.SidebarDivider import org.zotero.android.uicomponents.theme.CustomTheme @@ -113,6 +116,21 @@ internal fun PdfReaderPhoneMode( } } } + AnimatedContent(targetState = viewState.showPdfSearch, transitionSpec = { + createPdfSearchTransitionSpec() + }, label = "") { showScreen -> + if (showScreen) { + Column( + modifier = Modifier + .fillMaxSize() + .background(CustomTheme.colors.pdfAnnotationsFormBackground) + ) { + PdfReaderSearchScreen( + onBack = vMInterface::hidePdfSearch + ) + } + } + } } } @@ -127,3 +145,15 @@ private fun AnimatedContentTransitionScope.createSidebarTransitionSpec( sizeAnimationSpec = { _, _ -> tween() } )) } + +private fun AnimatedContentTransitionScope.createPdfSearchTransitionSpec(): ContentTransform { + val intOffsetSpec = tween() + return (slideInVertically(intOffsetSpec) { it } with + slideOutVertically(intOffsetSpec) { it }).using( + // Disable clipping since the faded slide-in/out should + // be displayed out of bounds. + SizeTransform( + clip = false, + sizeAnimationSpec = { _, _ -> tween() } + )) +} diff --git a/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderScreen.kt b/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderScreen.kt index 1dc24461..c393f5aa 100644 --- a/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderScreen.kt +++ b/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderScreen.kt @@ -126,9 +126,13 @@ internal fun PdfReaderScreen( onBack = onBack, onShowHideSideBar = viewModel::toggleSideBar, toPdfSettings = viewModel::navigateToPdfSettings, + showPdfSearch = viewState.showPdfSearch, toggleToolbarButton = viewModel::toggleToolbarButton, isToolbarButtonSelected = viewState.showCreationToolbar, showSideBar = viewState.showSideBar, + onShowHidePdfSearch = viewModel::togglePdfSearch, + viewModel = viewModel, + viewState = viewState ) } } diff --git a/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderTopBar.kt b/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderTopBar.kt index b78af48b..004eeba5 100644 --- a/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderTopBar.kt +++ b/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderTopBar.kt @@ -1,7 +1,10 @@ package org.zotero.android.pdf.reader +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import org.zotero.android.architecture.ui.CustomLayoutSize +import org.zotero.android.pdf.reader.pdfsearch.PdfReaderSearchPopup import org.zotero.android.uicomponents.Drawables import org.zotero.android.uicomponents.Strings import org.zotero.android.uicomponents.icon.IconWithPadding @@ -15,10 +18,15 @@ internal fun PdfReaderTopBar( onBack: () -> Unit, onShowHideSideBar: () -> Unit, toPdfSettings: () -> Unit, + onShowHidePdfSearch: () -> Unit, toggleToolbarButton:() -> Unit, isToolbarButtonSelected: Boolean, showSideBar: Boolean, + showPdfSearch: Boolean, + viewState: PdfReaderViewState, + viewModel: PdfReaderVMInterface ) { + val isTablet = CustomLayoutSize.calculateLayoutType().isTablet() NewCustomTopBar( backgroundColor = CustomTheme.colors.surface, leftContainerContent = listOf( @@ -46,9 +54,33 @@ internal fun PdfReaderTopBar( isSelected = isToolbarButtonSelected ) }, + { + if (isTablet) { + Box { + IconWithPadding( + drawableRes = Drawables.search_24px, + onClick = onShowHidePdfSearch + ) + if (viewState.showPdfSearch) { + PdfReaderSearchPopup( + viewState = viewState, + viewModel = viewModel, + ) + } + } + } else { + ToggleIconWithPadding( + drawableRes = Drawables.search_24px, + onToggle = { + onShowHidePdfSearch() + }, + isSelected = showPdfSearch + ) + } + }, { IconWithPadding(drawableRes = Drawables.settings_24px, onClick = toPdfSettings) - } + }, ) ) } diff --git a/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderVMInterface.kt b/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderVMInterface.kt index 388e0908..4e212f8a 100644 --- a/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderVMInterface.kt +++ b/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderVMInterface.kt @@ -47,4 +47,6 @@ interface PdfReaderVMInterface { fun onOutlineItemChevronTapped(outline: Outline) fun selectThumbnail(row: PdfReaderThumbnailRow) fun loadThumbnailPreviews(pageIndex: Int) + fun hidePdfSearch() + fun togglePdfSearch() } \ No newline at end of file diff --git a/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderViewModel.kt b/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderViewModel.kt index 6395d8d4..56700282 100644 --- a/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderViewModel.kt +++ b/app/src/main/java/org/zotero/android/pdf/reader/PdfReaderViewModel.kt @@ -47,6 +47,7 @@ import com.pspdfkit.preferences.PSPDFKitPreferences import com.pspdfkit.ui.PdfFragment import com.pspdfkit.ui.PdfUiFragment import com.pspdfkit.ui.PdfUiFragmentBuilder +import com.pspdfkit.ui.search.SearchResultHighlighter import com.pspdfkit.ui.special_mode.controller.AnnotationCreationController import com.pspdfkit.ui.special_mode.controller.AnnotationSelectionController import com.pspdfkit.ui.special_mode.controller.AnnotationTool @@ -135,6 +136,8 @@ import org.zotero.android.pdf.data.PdfAnnotationChanges import org.zotero.android.pdf.data.PdfReaderArgs import org.zotero.android.pdf.data.PdfReaderCurrentThemeEventStream import org.zotero.android.pdf.data.PdfReaderThemeDecider +import org.zotero.android.pdf.reader.pdfsearch.data.PdfReaderSearchArgs +import org.zotero.android.pdf.reader.pdfsearch.data.PdfReaderSearchResultSelected import org.zotero.android.pdf.reader.sidebar.data.Outline import org.zotero.android.pdf.reader.sidebar.data.PdfReaderOutlineOptionsWithChildren import org.zotero.android.pdf.reader.sidebar.data.PdfReaderSliderOptions @@ -221,6 +224,8 @@ class PdfReaderViewModel @Inject constructor( private var toolHistory = mutableListOf() + private lateinit var searchResultHighlighter: SearchResultHighlighter + val screenArgs: PdfReaderArgs by lazy { val argsEncoded = stateHandle.get(ARG_PDF_SCREEN).require() navigationParamsMarshaller.decodeObjectFromBase64(argsEncoded) @@ -330,8 +335,13 @@ class PdfReaderViewModel @Inject constructor( ) } } + } - + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEvent(result: PdfReaderSearchResultSelected) { + searchResultHighlighter.setSearchResults(listOf(result.searchResult)) + searchResultHighlighter.setSelectedSearchResult(result.searchResult) + this.pdfUiFragment.pageIndex = result.searchResult.pageIndex } private fun update(pdfSettings: PDFSettings) { @@ -386,6 +396,8 @@ class PdfReaderViewModel @Inject constructor( this.containerId = containerId this.annotationMaxSideSize = annotationMaxSideSize + searchResultHighlighter = SearchResultHighlighter(context) + if (this::pdfUiFragment.isInitialized) { replaceFragment() return @@ -413,6 +425,7 @@ class PdfReaderViewModel @Inject constructor( this@PdfReaderViewModel.pdfUiFragment.lifecycle.addObserver(object: DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { this@PdfReaderViewModel.pdfFragment = pdfUiFragment.pdfFragment!! + this@PdfReaderViewModel.pdfFragment.addDrawableProvider(searchResultHighlighter) addDocumentListenerOnInit() addOnAnnotationCreationModeChangeListener() setOnPreparePopupToolbarListener() @@ -2226,6 +2239,7 @@ class PdfReaderViewModel @Inject constructor( this@PdfReaderViewModel.pdfUiFragment.lifecycle.addObserver(object: DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { this@PdfReaderViewModel.pdfFragment = pdfUiFragment.pdfFragment!! + this@PdfReaderViewModel.pdfFragment.addDrawableProvider(searchResultHighlighter) addDocumentListener2() addOnAnnotationCreationModeChangeListener() setOnPreparePopupToolbarListener() @@ -3190,6 +3204,25 @@ class PdfReaderViewModel @Inject constructor( } } + + override fun togglePdfSearch() { + ScreenArguments.pdfReaderSearchArgs = PdfReaderSearchArgs( + pdfDocument = this.document, + configuration = pdfFragment.configuration + ) + updateState { + copy(showPdfSearch = !showPdfSearch) + } + + } + + override fun hidePdfSearch() { + updateState { + copy( + showPdfSearch = false + ) + } + } } data class PdfReaderViewState( @@ -3229,7 +3262,8 @@ data class PdfReaderViewState( val outlineSearchTerm: String = "", val isOutlineEmpty: Boolean = false, val thumbnailRows: ImmutableList = persistentListOf(), - val selectedThumbnail: PdfReaderThumbnailRow? = null + val selectedThumbnail: PdfReaderThumbnailRow? = null, + val showPdfSearch: Boolean = false, ) : ViewState { fun isAnnotationSelected(annotationKey: String): Boolean { diff --git a/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchPopup.kt b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchPopup.kt new file mode 100644 index 00000000..ec6431d3 --- /dev/null +++ b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchPopup.kt @@ -0,0 +1,78 @@ +package org.zotero.android.pdf.reader.pdfsearch + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties +import org.zotero.android.pdf.reader.PdfReaderVMInterface +import org.zotero.android.pdf.reader.PdfReaderViewState +import org.zotero.android.uicomponents.CustomScaffold +import org.zotero.android.uicomponents.theme.CustomTheme + +@Composable +internal fun PdfReaderSearchPopup( + viewState: PdfReaderViewState, + viewModel: PdfReaderVMInterface, +) { + val backgroundColor = CustomTheme.colors.cardBackground + Popup( + properties = PopupProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true, + focusable = true + ), + onDismissRequest = viewModel::hidePdfSearch, + popupPositionProvider = createPdfReaderSearchPopupPositionProvider(), + + ) { + CustomScaffold( + modifier = Modifier + .width(350.dp) + .height(530.dp) + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(16.dp), + ), + backgroundColor = backgroundColor, + ) { + PdfReaderSearchScreen(onBack = viewModel::hidePdfSearch) + } + } +} + +@Composable +private fun createPdfReaderSearchPopupPositionProvider() = object : PopupPositionProvider { + val localDensity = LocalDensity.current + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + val extraXOffset = with(localDensity) { + 12.dp.toPx() + }.toInt() + val extraYOffset = with(localDensity) { + 8.dp.toPx() + }.toInt() + + val xOffset = windowSize.width - popupContentSize.width - extraXOffset + val yOffset = anchorBounds.bottom + extraYOffset + + return IntOffset( + x = xOffset, + y = yOffset + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchScreen.kt b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchScreen.kt new file mode 100644 index 00000000..fbdc4ab3 --- /dev/null +++ b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchScreen.kt @@ -0,0 +1,165 @@ +package org.zotero.android.pdf.reader.pdfsearch + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import org.zotero.android.pdf.reader.pdfsearch.data.PdfReaderSearchItem +import org.zotero.android.uicomponents.Plurals +import org.zotero.android.uicomponents.Strings +import org.zotero.android.uicomponents.foundation.quantityStringResource +import org.zotero.android.uicomponents.misc.NewDivider +import org.zotero.android.uicomponents.textinput.SearchBar +import org.zotero.android.uicomponents.theme.CustomPalette +import org.zotero.android.uicomponents.theme.CustomTheme +import org.zotero.android.uicomponents.theme.CustomThemeWithStatusAndNavBars + +@Composable +internal fun PdfReaderSearchScreen( + viewModel: PdfReaderSearchViewModel = hiltViewModel(), + onBack: () -> Unit, +) { + viewModel.init() + viewModel.setOsTheme(isDark = isSystemInDarkTheme()) + val viewState by viewModel.viewStates.observeAsState(PdfReaderSearchViewState()) + val viewEffect by viewModel.viewEffects.observeAsState() + CustomThemeWithStatusAndNavBars(isDarkTheme = viewState.isDark) { + LaunchedEffect(key1 = viewEffect) { + when (viewEffect?.consume()) { + null -> Unit + is PdfReaderSearchViewEffect.OnBack -> { + onBack() + } + + } + } + + LazyColumn( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + Spacer(modifier = Modifier.height(16.dp)) + PdfReaderSearchBar( + searchValue = viewState.searchTerm, + onSearch = viewModel::onSearch, + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + PdfReaderSearchTable( + viewModel = viewModel, + viewState = viewState, + ) + + item { + if (viewState.searchResults.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = quantityStringResource( + id = Plurals.pdf_search_matches, viewState.searchResults.size + ), + style = CustomTheme.typography.newFootnote, + color = CustomPalette.DarkGrayColor + ) + } + Spacer(modifier = Modifier.height(8.dp)) + } + } + + } +} + +internal fun LazyListScope.PdfReaderSearchTable( + viewState: PdfReaderSearchViewState, + viewModel: PdfReaderSearchViewModel, +) { + items(viewState.searchResults) { item -> + PdfReaderSearchRow(searchItem = item, onItemTapped = {viewModel.onItemTapped(item)}) + } + +} + +@Composable +private fun PdfReaderSearchRow( + searchItem: PdfReaderSearchItem, + onItemTapped: () -> Unit, +) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = onItemTapped, + ) + ) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + modifier = Modifier.align(Alignment.End), + text = stringResource(Strings.page) + " ${searchItem.pageNumber + 1}", + style = CustomTheme.typography.newCaptionOne, + color = CustomTheme.colors.zoteroBlueWithDarkMode + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + modifier = Modifier, + text = searchItem.annotatedString, + style = CustomTheme.typography.newBody, + color = CustomTheme.colors.defaultTextColor, + ) + Spacer(modifier = Modifier.height(8.dp)) + NewDivider( + modifier = Modifier + ) + + } +} + +@Composable +internal fun PdfReaderSearchBar( + searchValue: String, + onSearch: (String) -> Unit, +) { + var searchBarTextFieldState by remember { + mutableStateOf( + TextFieldValue( + searchValue + ) + ) + } + val searchBarOnInnerValueChanged: (TextFieldValue) -> Unit = { + searchBarTextFieldState = it + onSearch(it.text) + } + SearchBar( + modifier = Modifier.padding(horizontal = 16.dp), + hint = stringResource(id = Strings.pdf_search_title), + onInnerValueChanged = searchBarOnInnerValueChanged, + textFieldState = searchBarTextFieldState, + backgroundColor = CustomTheme.colors.pdfAnnotationsSearchBarBackground + ) +} diff --git a/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchViewModel.kt b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchViewModel.kt new file mode 100644 index 00000000..7db50ea2 --- /dev/null +++ b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/PdfReaderSearchViewModel.kt @@ -0,0 +1,153 @@ +package org.zotero.android.pdf.reader.pdfsearch + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.lifecycle.viewModelScope +import com.pspdfkit.document.search.CompareOptions +import com.pspdfkit.document.search.SearchOptions +import com.pspdfkit.document.search.SearchResult +import com.pspdfkit.document.search.TextSearch +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.EventBus +import org.zotero.android.architecture.BaseViewModel2 +import org.zotero.android.architecture.ScreenArguments +import org.zotero.android.architecture.ViewEffect +import org.zotero.android.architecture.ViewState +import org.zotero.android.architecture.coroutines.Dispatchers +import org.zotero.android.pdf.data.PdfReaderCurrentThemeEventStream +import org.zotero.android.pdf.data.PdfReaderThemeDecider +import org.zotero.android.pdf.reader.pdfsearch.data.PdfReaderSearchItem +import org.zotero.android.pdf.reader.pdfsearch.data.PdfReaderSearchResultSelected +import javax.inject.Inject + +@HiltViewModel +internal class PdfReaderSearchViewModel @Inject constructor( + private val dispatchers: Dispatchers, + private val pdfReaderCurrentThemeEventStream: PdfReaderCurrentThemeEventStream, + private val pdfReaderThemeDecider: PdfReaderThemeDecider, +) : BaseViewModel2(PdfReaderSearchViewState()) { + + private val onSearchStateFlow = MutableStateFlow("") + + private var pdfReaderThemeCancellable: Job? = null + private lateinit var searchResults: List + + private lateinit var textSearch: TextSearch + private val searchOptions: SearchOptions = SearchOptions.Builder() + .compareOptions(CompareOptions.CASE_INSENSITIVE, CompareOptions.DIACRITIC_INSENSITIVE) + .build() + + private fun startObservingTheme() { + this.pdfReaderThemeCancellable = pdfReaderCurrentThemeEventStream.flow() + .onEach { data -> + updateState { + copy(isDark = data!!.isDark) + } + } + .launchIn(viewModelScope) + } + + + fun init() = initOnce { + updateState { + copy(isDark = pdfReaderCurrentThemeEventStream.currentValue()!!.isDark) + } + startObservingTheme() + + + val args = ScreenArguments.pdfReaderSearchArgs + textSearch = TextSearch(args.pdfDocument, args.configuration) + + onSearchStateFlow + .drop(1) + .debounce(150) + .map { text -> + search(text) + } + .launchIn(viewModelScope) + + } + + private fun search(term: String) { + if (!term.isEmpty()) { + viewModelScope.launch { + withContext(dispatchers.io) { + searchResults = textSearch.performSearch(term, searchOptions) + val rows = searchResults.mapNotNull { + val snippet = it.snippet ?: return@mapNotNull null + + val annotatedString = buildAnnotatedString { + val highlightStart = snippet.rangeInSnippet.startPosition + val highlightEnd = snippet.rangeInSnippet.endPosition + val previewText = snippet.text + append(previewText) + addStyle( + style = SpanStyle(background = Color.Yellow), + start = highlightStart, + end = highlightEnd + ) + } + PdfReaderSearchItem( + pageNumber = it.pageIndex, + annotatedString = annotatedString + ) + } + viewModelScope.launch { + updateState { + copy(searchResults = rows) + } + } + + } + } + + + } else { + updateState { + copy( + searchResults = emptyList() + ) + } + } + } + + fun onSearch(text: String) { + updateState { + copy(searchTerm = text) + } + onSearchStateFlow.tryEmit(text) + } + + fun onItemTapped(searchItem: PdfReaderSearchItem) { + val searchResult = searchResults[viewState.searchResults.indexOf(searchItem)] + + triggerEffect(PdfReaderSearchViewEffect.OnBack) + EventBus.getDefault().post(PdfReaderSearchResultSelected(searchResult)) + + } + + fun setOsTheme(isDark: Boolean) { + pdfReaderThemeDecider.setCurrentOsTheme(isOsThemeDark = isDark) + } + +} + +internal data class PdfReaderSearchViewState( + val searchTerm: String = "", + val searchResults: List = emptyList(), + val isDark: Boolean = false, +) : ViewState + +internal sealed class PdfReaderSearchViewEffect : ViewEffect { + object OnBack : PdfReaderSearchViewEffect() +} \ No newline at end of file diff --git a/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchArgs.kt b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchArgs.kt new file mode 100644 index 00000000..fb0cbd10 --- /dev/null +++ b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchArgs.kt @@ -0,0 +1,9 @@ +package org.zotero.android.pdf.reader.pdfsearch.data + +import com.pspdfkit.configuration.PdfConfiguration +import com.pspdfkit.document.PdfDocument + +data class PdfReaderSearchArgs( + val pdfDocument: PdfDocument, + val configuration: PdfConfiguration +) \ No newline at end of file diff --git a/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchItem.kt b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchItem.kt new file mode 100644 index 00000000..190a0a74 --- /dev/null +++ b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchItem.kt @@ -0,0 +1,5 @@ +package org.zotero.android.pdf.reader.pdfsearch.data + +import androidx.compose.ui.text.AnnotatedString + +data class PdfReaderSearchItem(val pageNumber: Int, val annotatedString: AnnotatedString) diff --git a/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchResultSelected.kt b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchResultSelected.kt new file mode 100644 index 00000000..a9d09563 --- /dev/null +++ b/app/src/main/java/org/zotero/android/pdf/reader/pdfsearch/data/PdfReaderSearchResultSelected.kt @@ -0,0 +1,5 @@ +package org.zotero.android.pdf.reader.pdfsearch.data + +import com.pspdfkit.document.search.SearchResult + +data class PdfReaderSearchResultSelected(val searchResult: SearchResult) diff --git a/build.gradle.kts b/build.gradle.kts index 789f1e25..903760a0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,8 +8,8 @@ buildscript { gradlePluginPortal() } dependencies { - classpath("com.android.tools.build:gradle:8.2.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") + classpath("com.android.tools.build:gradle:8.2.2") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21") classpath(Libs.Kotlin.serialization) classpath(Libs.Firebase.Crashlytics.crashlyticsGradle) classpath(Libs.googleServices) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 3e5bf28f..881ac5ea 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -9,8 +9,8 @@ plugins { dependencies { // This constant is duplicated in root/build.gradle.kts. Make sure to also update there - implementation("com.android.tools.build:gradle:8.2.1") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") + implementation("com.android.tools.build:gradle:8.2.2") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21") // Without this Hilts aggregate dependencies task is unable to run. implementation("com.squareup:javapoet:1.13.0") } diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 85e15b5a..155cf5cd 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -20,7 +20,7 @@ object Libs { } object Compose { - const val compileVersion = "1.5.4" + const val compileVersion = "1.5.7" private const val version = "1.6.0-rc01" const val activity = "androidx.activity:activity-compose:1.8.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ad8926fa..b5ec9b53 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Aug 15 17:35:12 GST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME