Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit f727d455 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: extract search state use cases from SearchViewModelV2

Move query/tab/suggestion decisions into domain use cases and a UI reducer to keep the ViewModel focused and improve testability.

Add unit coverage for the new search state logic.
parent 4bbc9166
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.domain.search

import foundation.e.apps.data.preference.AppLoungePreference
import foundation.e.apps.data.search.SuggestionSource
import javax.inject.Inject

class FetchSearchSuggestionsUseCase @Inject constructor(
    private val suggestionSource: SuggestionSource,
    private val appLoungePreference: AppLoungePreference,
) {
    suspend operator fun invoke(query: String): List<String> {
        if (query.isBlank() || !appLoungePreference.isPlayStoreSelected()) {
            return emptyList()
        }
        return suggestionSource.suggest(query)
    }
}
+58 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.domain.search

import foundation.e.apps.ui.search.v2.SearchTabType
import javax.inject.Inject

class PrepareSearchSubmissionUseCase @Inject constructor(
    private val resolveVisibleSearchTabsUseCase: ResolveVisibleSearchTabsUseCase,
) {
    operator fun invoke(
        submittedQuery: String,
        selectedTab: SearchTabType?,
        currentVersion: Int,
    ): SearchSubmissionResult {
        val trimmedQuery = submittedQuery.trim()
        val visibleTabs = resolveVisibleSearchTabsUseCase()
        val resolvedSelectedTab = selectedTab?.takeIf { visibleTabs.contains(it) }
            ?: visibleTabs.firstOrNull()
        val shouldIncrementVersion = trimmedQuery.isNotEmpty()
        val nextVersion = if (shouldIncrementVersion) currentVersion + 1 else currentVersion
        val hasSubmittedSearch = trimmedQuery.isNotEmpty() && visibleTabs.isNotEmpty()
        val searchRequest = if (hasSubmittedSearch) {
            SearchRequest(
                query = trimmedQuery,
                visibleTabs = visibleTabs,
                version = nextVersion,
            )
        } else {
            null
        }

        return SearchSubmissionResult(
            trimmedQuery = trimmedQuery,
            visibleTabs = visibleTabs,
            selectedTab = resolvedSelectedTab,
            hasSubmittedSearch = hasSubmittedSearch,
            nextVersion = nextVersion,
            searchRequest = searchRequest,
        )
    }
}
+41 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.domain.search

import foundation.e.apps.data.Stores
import foundation.e.apps.data.enums.Source.OPEN_SOURCE
import foundation.e.apps.data.enums.Source.PLAY_STORE
import foundation.e.apps.data.enums.Source.PWA
import foundation.e.apps.ui.search.v2.SearchTabType
import javax.inject.Inject

class ResolveVisibleSearchTabsUseCase @Inject constructor(
    private val stores: Stores,
) {
    operator fun invoke(): List<SearchTabType> {
        return stores.getStores().mapNotNull { (key, _) ->
            when (key) {
                PLAY_STORE -> SearchTabType.COMMON_APPS
                OPEN_SOURCE -> SearchTabType.OPEN_SOURCE
                PWA -> SearchTabType.PWA
                else -> null
            }
        }
    }
}
+38 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.domain.search

import foundation.e.apps.ui.search.v2.SearchTabType

data class SearchSubmissionResult(
    val trimmedQuery: String,
    val visibleTabs: List<SearchTabType>,
    val selectedTab: SearchTabType?,
    val hasSubmittedSearch: Boolean,
    val nextVersion: Int,
    val searchRequest: SearchRequest?,
)

data class StoreSelectionUpdate(
    val visibleTabs: List<SearchTabType>,
    val selectedTab: SearchTabType?,
    val hasSubmittedSearch: Boolean,
    val suggestionsEnabled: Boolean,
    val searchRequest: SearchRequest?,
)
+62 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.domain.search

import foundation.e.apps.data.preference.AppLoungePreference
import foundation.e.apps.ui.search.v2.SearchTabType
import javax.inject.Inject

class UpdateSearchForStoreSelectionUseCase @Inject constructor(
    private val resolveVisibleSearchTabsUseCase: ResolveVisibleSearchTabsUseCase,
    private val appLoungePreference: AppLoungePreference,
) {
    operator fun invoke(
        currentQuery: String,
        selectedTab: SearchTabType?,
        hasSubmittedSearch: Boolean,
        currentVersion: Int,
    ): StoreSelectionUpdate {
        val visibleTabs = resolveVisibleSearchTabsUseCase()
        val resolvedSelectedTab = selectedTab?.takeIf { visibleTabs.contains(it) }
            ?: visibleTabs.firstOrNull()
        val updatedHasSubmittedSearch = hasSubmittedSearch && visibleTabs.isNotEmpty()
        val shouldUpdateRequest = hasSubmittedSearch && currentQuery.isNotBlank()
        val searchRequest = when {
            shouldUpdateRequest && visibleTabs.isNotEmpty() -> SearchRequest(
                query = currentQuery,
                visibleTabs = visibleTabs,
                version = currentVersion,
            )
            !hasSubmittedSearch -> SearchRequest(
                query = "",
                visibleTabs = visibleTabs,
                version = currentVersion,
            )
            else -> null
        }

        return StoreSelectionUpdate(
            visibleTabs = visibleTabs,
            selectedTab = resolvedSelectedTab,
            hasSubmittedSearch = updatedHasSubmittedSearch,
            suggestionsEnabled = appLoungePreference.isPlayStoreSelected(),
            searchRequest = searchRequest,
        )
    }
}
Loading